在ARM中快速将16位大端转换为小端

7

我需要将大型的16位整数数组从big-endian格式转换为little-endian格式。

现在我使用以下函数进行转换:

inline void Reorder16bit(const uint8_t * src, uint8_t * dst)
{
    uint16_t value = *(uint16_t*)src;
    *(uint16_t*)dst = value >> 8 | value << 8;
}

void Reorder16bit(const uint8_t * src, size_t size, uint8_t * dst)
{
    assert(size%2 == 0);
    for(size_t i = 0; i < size; i += 2)
        Reorder16bit(src + i, dst + i);
}

我使用GCC。目标平台是ARMv7(树莓派2B)。

有没有什么方法可以进行优化?

这个转换是为了加载音频样本,可以是小端或大端格式。当然,现在它不是瓶颈,但它占用了总处理时间的约10%。我认为对于这样一个简单的操作来说太多了。


6
这真的是一个瓶颈吗?你进行过测量吗?你进行过性能分析吗? - Some programmer dude
如果我没记错的话,有一个专门的汇编指令可以做到这一点!但是我不记得它的名字了。 - hanshenrik
2
@hanshenrik:在x86-64上使用bswap。或者在GCC中使用__builtin_bswap - Jon Purdy
3
为什么要使用那些愚蠢的强制类型转换?例如在其中一个中,你正在丢弃const限定符,这正是不使用C风格强制类型转换的原因。你已经有了一个字节数组,为什么要从可能未对齐的地址中提取更大的类型,只是为了将字节在该更大类型内部移位并重新存储它们?只需考虑输入数组中的每个字节应该放在输出数组的哪个字节位置即可! - Ulrich Eckhardt
7
你的 Reorder16bit 代码违反了严格别名规则。你需要使用 -fno-strict-aliasing 并添加一个对齐检查,以确保正确操作,但这可能会减慢其余代码的运行速度。更好的选择是编写简单、正确的代码并告诉编译器进行优化,这就是编译器存在的目的。 - M.M
显示剩余2条评论
5个回答

8
如果你想提高代码的性能,可以采取以下措施:
1)一次处理4个字节:
inline void Reorder16bit(const uint8_t * src, uint8_t * dst)
{
    uint16_t value = *(uint16_t*)src;
    *(uint16_t*)dst = value >> 8 | value << 8;
}

inline void Reorder16bit2(const uint8_t * src, uint8_t * dst)
{
    uint32_t value = *(uint32_t*)src;
    *(size_t*)dst = (value & 0xFF00FF00) >> 8 | (value & 0x00FF00FF) << 8;
}

void Reorder16bit(const uint8_t * src, size_t size, uint8_t * dst)
{
    assert(size%2 == 0);

    size_t alignedSize = size/4*4;
    for(size_t i = 0; i < alignedSize; i += 4)
        Reorder16bit2(src + i, dst + i);
    for(size_t i = alignedSize; i < size; i += 2)
        Reorder16bit(src + i, dst + i);
}

如果您使用64位平台,可以以同样的方式一次处理8个字节。
2)ARMv7平台支持称为NEON的SIMD指令。使用它们,您可以使代码比1)中更快。
inline void Reorder16bit(const uint8_t * src, uint8_t * dst)
{
    uint16_t value = *(uint16_t*)src;
    *(uint16_t*)dst = value >> 8 | value << 8;
}

inline void Reorder16bit8(const uint8_t * src, uint8_t * dst)
{
    uint8x16_t _src = vld1q_u8(src);
    vst1q_u8(dst, vrev16q_u8(_src));
}

void Reorder16bit(const uint8_t * src, size_t size, uint8_t * dst)
{
    assert(size%2 == 0);

    size_t alignedSize = size/16*16;
    for(size_t i = 0; i < alignedSize; i += 16)
        Reorder16bit8(src + i, dst + i);
    for(size_t i = alignedSize; i < size; i += 2)
        Reorder16bit(src + i, dst + i);
}

很有趣。我会检查它。 - user5480682

8

https://goo.gl/4bRGNh

int swap(int b) {
  return __builtin_bswap16(b);
}

变成

swap(int):
        rev16   r0, r0
        uxth    r0, r0
        bx      lr

所以你的代码可以写成(gcc-explorer: https://goo.gl/HFLdMb

void fast_Reorder16bit(const uint16_t * src, size_t size, uint16_t * dst)
{
    assert(size%2 == 0);
    for(size_t i = 0; i < size; i++)
        dst[i] = __builtin_bswap16(src[i]);
} 

这应该生成循环

.L13:
        ldrh    r4, [r0, r3]
        rev16   r4, r4
        strh    r4, [r2, r3]    @ movhi
        adds    r3, r3, #2
        cmp     r3, r1
        bne     .L13

请访问GCC内置文档,了解更多关于__builtin_bswap16的信息。

Neon建议(经过测试,gcc-explorer: https://goo.gl/fLNYuc):

void neon_Reorder16bit(const uint8_t * src, size_t size, uint8_t * dst)
{
  assert(size%16 == 0);
  //uint16x8_t vld1q_u16 (const uint16_t *) 
  //vrev64q_u16(uint16x8_t vec);
  //void vst1q_u16 (uint16_t *, uint16x8_t) 
  for (size_t i = 0; i < size; i += 16)
    vst1q_u8(dst + i, vrev16q_u8(vld1q_u8(src + i)));
}

变成

.L23:
        adds    r5, r0, r3
        adds    r4, r2, r3
        adds    r3, r3, #16
        vld1.8  {d16-d17}, [r5]
        cmp     r1, r3
        vrev16.8        q8, q8
        vst1.8  {d16-d17}, [r4]
        bhi     .L23

在这里查看有关neon内部函数的更多信息:https://gcc.gnu.org/onlinedocs/gcc-4.4.1/gcc/ARM-NEON-Intrinsics.html

来自ARM ARM A8.8.386的奖励:

VREV16(半字中的矢量反转)将向量中每个半字节的8位元素顺序颠倒,并将结果放置在相应的目标向量中。

VREV32(字中的矢量反转)将向量中每个字节或16位元素的顺序颠倒,并将结果放置在相应的目标向量中。

VREV64(双字中的矢量反转)将向量中每个双字节的8位、16位或32位元素顺序颠倒,并将结果放置在相应的目标向量中。

除了大小之外,没有数据类型区别。

vrev diagram


1
你使用vrev64q_u16()的代码结果不正确。也许你想用vrev16q_u8()代替它? - ErmIg
1
除了这里的建议,加载/存储也是其中很大一部分。如果您正在重新评估、过滤等,NEON 可以在交换数据时处理数据,而 NEON/SIMD 适用于音频处理等应用程序。典型的音频算法只涉及附近的样本值。因此,基于过程的模式不会是最优的,但使用具有更大的加载/存储 for 循环的 NEON 数据类型的内联函数可能会更好。 - artless noise

6
如果是针对ARM的话,有一个REV指令,特别是REV16可以同时处理两个16位整数。

5
编译器使用这个来实现 __builtin_bswap16 - Jens
4
值得注意的是,即使是老版的GCC 4.8(使用“-O2”选项),也能识别这个习语,并将问题中的代码编译为围绕单个“rev16”指令的装载/存储循环。 - Notlikethat

6

我对ARM指令集不是很了解,但我猜测有一些特殊的指令可以进行大小端转换。 显然,ARMv7具有类似rev等的功能。

您尝试过编译器内在函数__builtin_bswap16吗?它应该编译为CPU特定的代码,例如在ARM上的rev。此外,它还帮助编译器识别您实际上正在进行字节交换,并使用该知识执行其他优化,例如在像y = swap(x); y&= some_value; x = swap(y);这样的情况下完全消除冗余的字节交换。

我谷歌了一下,这个主题讨论了一个优化潜力问题根据这个讨论,如果CPU支持vrev NEON指令,则编译器还可以矢量化转换。


你的例子中,交换操作有何冗余之处? - JimmyB
@HannoBinder 我假设 some_value 是一个常量,所以不需要交换 x、加上常量再交换回来,编译器可以计算出一个已经交换的常量并将其与 x 结合。另一种情况是使用位运算,例如从 swap(x) 中屏蔽一些位。 - Jens
1
@Jens 交换常量?这在进位运算中如何工作?如果任一字节发生进位条件,结果将不正确。 - nitro2k01
@Jens,问题中的示例是OP版本的字节交换函数,而不是他希望对交换后的数据执行的操作... - nitro2k01
@nitro2k01 对的,这是字节交换操作。但我认为他要对数据进行某些操作,而编译器可能会进行优化。我不认为他只是为了好玩而交换字节。 - Jens
显示剩余3条评论

2

你可能想要进行测量以确定哪个更快,但是Reorder16bit的另一种替代方法是:

*(uint16_t*)dst = 256 * src[0] + src[1];

假设您的本机int是小端字节序。另一个可能性是:
dst[0] = src[1];
dst[1] = src[0];

1
最初,我使用了您的第二个变体。它需要在内存中加载2次和存储2次。我的变体只需要在内存中加载1次和存储1次,并在寄存器之间进行3次位操作。令人惊讶的是,由于内存访问缓慢,第二个变体更快。 - user5480682
第二个变量由于别名而较慢。如果添加临时变量,并在任何写入之前进行两次读取,则编译器应该能够更好地进行优化。 - Ben Voigt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接