【问题标题】:Fast conversion of 16-bit big-endian to little-endian in ARMARM中16位大端到小端的快速转换
【发布时间】:2016-02-29 03:50:02
【问题描述】:

我需要将 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 (Raspberry Phi 2B)。

有什么办法可以优化吗?

加载音频样本时需要这种转换,这些样本可以是小端格式也可以是大端格式。当然它现在不是瓶颈,但它需要大约 10% 的总处理时间。而且我认为对于这么简单的操作来说太过分了。

【问题讨论】:

  • 这真的是瓶颈吗?你量过吗?你有简介吗?
  • 如果我没记错的话,有一个专门的组装说明可以做到这一点!但我不记得名字了
  • @hanshenrik: x86-64 上的bswap。或 GCC 中的 __builtin_bswap
  • 你为什么要使用那些愚蠢的演员表?例如,您要丢弃 const 限定符,这正是不使用 C 样式强制转换的原因。您已经有一个字节数组,为什么要从某个可能未对齐的地址中提取更大的类型,只是为了移动该更大类型中的字节并再次存储它们?只要想想输入数组中的哪个字节属于输出数组的哪个字节!
  • 您的Reorder16bit 代码违反了严格的别名规则。您必须使用-fno-strict-aliasing,加上对齐检查,以保证正确操作,这可能会减慢您的其余代码。更好的选择是编写简单、正确的代码并告诉编译器对其进行优化。这就是编译器的用途。

标签: c++ arm simd neon


【解决方案1】:

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]);
} 

应该是for循环

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

GCC builtin docs 上阅读有关__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

在此处查看有关霓虹内在函数的更多信息:https://gcc.gnu.org/onlinedocs/gcc-4.4.1/gcc/ARM-NEON-Intrinsics.html

来自 ARM ARM A8.8.386 的奖励:

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

VREV32(Vector Reverse in words)反转向量每个字中 8 位或 16 位元素的顺序,并将结果放入相应的目标向量中。

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

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

【讨论】:

  • 您使用 vrev64q_u16() 的代码给出了错误的结果。可能你的意思是 vrev16q_u8() 而不是这个?
  • 和这里的建议一样,加载/存储是其中很大一部分。如果您正在重新评级、过滤等,NEON 可以在进行交换时处理数据,NEON/SIMD 用于音频处理等应用程序。典型的音频算法只涉及附近的样本值。所以基于过程的模式不会是最佳的,但是一些具有 NEON 数据类型的内联函数具有更大的加载/存储for 循环可能会更好。
【解决方案2】:

我对ARM指令集了解不多,但我想有一些特殊的指令可以进行字节序转换。显然,ARMv7 有 rev 之类的东西。

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

我用谷歌搜索了一下,this thread discusses an issue with the optimization potentialAccording to this discussion,如果CPU支持vrevNEON指令,编译器也可以向量化转换。

【讨论】:

  • 您的示例中的交换如何以任何方式冗余?
  • @HannoBinder 我假设 some_value 是一个常量,因此编译器可以计算一个交换后的常量并将其与 x 组合,而不是交换 x,添加常量并交换回来。另一种情况是使用位操作,例如从交换(x)中屏蔽一些位。
  • 好吧,我没有意识到some_value 是用来表示一个编译时常量。在这种情况下,您当然可能是正确的。
  • @Jens 交换常量?这应该如何与carry一起使用?如果任一字节发生进位条件,则结果不正确。
  • @nitro2k01 是的,你是对的。但它适用于位操作,例如二进制和。这是第一个链接中的示例,gcc 实现了它。
【解决方案3】:

如果它专门用于 ARM,则有一条 REV 指令,特别是 REV16,它将一次执行两个 16 位整数。

【讨论】:

  • @David 编译器使用它来实现__builtin_bswap16
  • 值得注意的是,即使是旧的 GCC 4.8(-O2)也能识别成语并将问题中的代码编译为围绕单个 rev16 指令的加载/存储循环。
【解决方案4】:

如果您想提高代码的性能,您可以进行以下操作:

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);
}

【讨论】:

  • 这很有趣。我会检查的。
【解决方案5】:

您想要测量哪个更快,但Reorder16bit 的替代机构是

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

假设你的原生整数是小端的。另一种可能性:

dst[0] = src[1];
dst[1] = src[0];

【讨论】:

  • 最初,我使用了您的第二个变体。它需要 2 次加载和 2 次存储在内存中。我的变体需要 1 次加载和 1 次内存以及寄存器之间的 3 位操作。令人惊讶的是,由于内存访问速度慢,第二种变体更快。
  • 第二个变体由于混叠而速度很慢。如果您添加临时变量,并在任何写入之前进行两次读取,编译器应该能够更好地优化。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-17
  • 1970-01-01
相关资源
最近更新 更多