RGBA 到 ABGR:iOS/Xcode 的内联臂霓虹灯汇编

RGBA to ABGR: Inline arm neon asm for iOS/Xcode

这段代码(非常相似的代码,还没有尝试完全这段代码)使用Android NDK编译,但不是Xcode/armv7+arm64/iOS

评论错误:

uint32_t *src;
uint32_t *dst;

#ifdef __ARM_NEON
__asm__ volatile(
    "vld1.32 {d0, d1}, [%[src]] \n" // error: Vector register expected
    "vrev32.8 q0, q0            \n" // error: Unrecognized instruction mnemonic
    "vst1.32 {d0, d1}, [%[dst]] \n" // error: Vector register expected
    :
    : [src]"r"(src), [dst]"r"(dst)
    : "d0", "d1"
    );
#endif

这段代码有什么问题?

编辑 1:

我使用内部函数重写了代码:

uint8x16_t x = vreinterpretq_u8_u32(vld1q_u32(src));
uint8x16_t y = vrev32q_u8(x);
vst1q_u32(dst, vreinterpretq_u32_u8(y));

拆解后,我得到以下,这是我已经尝试过的变体:

vld1.32 {d16, d17}, [r0]!
vrev32.8    q8, q8
vst1.32 {d16, d17}, [r1]!

所以我的代码现在看起来像这样,但给出了完全相同的错误:

__asm__ volatile("vld1.32 {d0, d1}, [%0]! \n"
                 "vrev32.8 q0, q0         \n"
                 "vst1.32 {d0, d1}, [%1]! \n"
                 :
                 : "r"(src), "r"(dst)
                 : "d0", "d1"
                 );

EDIT2:

翻阅反汇编,居然发现了第二个版本的函数。事实证明,arm64 使用的指令集略有不同。例如,arm64 程序集使用 rev32.16b v0, v0 代替。整个功能列表(我无法确定)如下:

_My_Function:
cmp     w2, #0
add w9, w2, #3
csel    w8, w9, w2, lt
cmp     w9, #7
b.lo    0x3f4
asr w9, w8, #2
ldr     x8, [x0]
mov  w9, w9
lsl x9, x9, #2
ldr q0, [x8], #16
rev32.16b   v0, v0
str q0, [x1], #16
sub x9, x9, #16
cbnz    x9, 0x3e0
ret

我已经成功发布了几个 iOS 应用程序,它们使用 ARM 汇编语言和内联代码,这是最令人沮丧的方式。 Apple 仍然要求应用程序同时支持 ARM32 和 ARM64 设备。由于默认情况下代码将构建为 ARM32 和 ARM64(除非您更改了编译选项),因此您需要设计能够在两种模式下成功编译的代码。正如您所注意到的,ARM64 是一种完全不同的助记符格式和寄存器模型。有两种简单的解决方法:

1) 使用 NEON 内在函数编写代码。 ARM 指定原始 ARM32 内在函数对于 ARMv8 目标将保持大部分不变,因此可以编译为 ARM32 和 ARM64 代码。这是 safest/easiest 选项。

2) 为您的汇编语言代码编写内联代码或单独的“.S”模块。处理2种编译模式,使用“#ifdef __arm64__”和“#ifdef __arm__”来区分两个指令集。

正如对原始问题的编辑中所述,事实证明我需要针对 arm64 和 armv7 的不同汇编实现。

#ifdef __ARM_NEON
  #if __LP64__
asm volatile("ldr q0, [%0], #16  \n"
             "rev32.16b v0, v0   \n"
             "str q0, [%1], #16  \n"
             : "=r"(src), "=r"(dst)
             : "r"(src), "r"(dst)
             : "d0", "d1"
             );
  #else
asm volatile("vld1.32 {d0, d1}, [%0]! \n"
             "vrev32.8 q0, q0         \n"
             "vst1.32 {d0, d1}, [%1]! \n"
             : "=r"(src), "=r"(dst)
             : "r"(src), "r"(dst)
             : "d0", "d1"
             );
  #endif
#else

我 post 在原始 post 中编写的内在函数代码生成了非常好的汇编,并且还为我生成了 arm64 版本,所以使用内在函数可能是更好的主意将来。

内部函数显然是在 ARM(32 位)和 AArch64 之间为 NEON 使用相同代码的唯一方法。

不使用的原因有很多: https://gcc.gnu.org/wiki/DontUseInlineAsm

不幸的是,当前的编译器通常在 ARM / AArch64 内部函数方面做得很差,这令人惊讶,因为它们在优化 x86 SSE/AVX 内部函数和 PowerPC Altivec 方面做得非常出色。 他们通常在简单的情况下做得很好,但很容易引入额外的 store/reloads.

理论上,使用内在函数,您应该获得良好的 asm 输出,并且它让编译器在向量加载和存储之间安排指令,这对有序内核有很大帮助。 (或者您可以在您手动安排的内联 asm 中编写一个完整的循环。)

ARM's official documentation:

Although it is technically possible to optimize NEON assembly by hand, this can be very difficult because the pipeline and memory access timings have complex inter-dependencies. Instead of hand assembly, ARM strongly recommends the use of intrinsics


如果您确实使用了内联汇编,请通过正确使用来避免未来的痛苦。

很容易编写恰好可以工作但不安全的内联 asm。未来的源代码更改(有时是未来的编译器优化),因为约束不能准确描述 asm 的作用。症状会很奇怪,这种上下文相关的错误甚至会导致单元测试通过但主程序中的代码错误。 (反之亦然)。

不会在当前构建中造成任何缺陷的潜在错误仍然是错误,并且在 Whosebug 答案中是一个非常糟糕的事情,可以作为示例复制到其他上下文中。 @bitwise 的问题和自答中的代码都有这样的错误。

问题中的内联 asm 不安全,因为它会修改内存并告知编译器。这可能只体现在一个循环中,该循环从 C 中的 dst 中读取在内联 asm 之前和之后。但是,它很容易修复,这样做可以让我们删除 volatile(以及它缺少的 `"memory" 破坏),以便编译器可以更好地优化(但与内在函数相比仍然有很大的局限性)。

volatile 应该 prevent reordering relative to memory accesses,所以它可能不会在相当人为的情况之外发生。但这很难证明。


以下针对 ARM 和 AArch64 进行编译(如果在 AArch64 上针对 ILP32 进行编译可能会失败,不过,我忘记了这种可能性)。使用 -funroll-loops 导致 gcc 选择不同的寻址模式,并且 而不是 强制 dst++; src++; 在每个内联 asm 语句之间发生。 (这对于 asm volatile 可能是不可能的)。

我使用了内存操作数,因此编译器知道内存是输入和输出,giving the compiler the option to use auto-increment / decrement addressing modes。这比使用寄存器中的指针作为输入操作数所能做的任何事情都要好,因为它允许展开循环。

这仍然不会让编译器在相应加载到 software pipeline the loop for in-order cores 之后安排存储许多指令,因此它可能只会在乱序的 ARM 芯片上正常执行。

void bytereverse32(uint32_t *dst32, const uint32_t *src32, size_t len)
{
    typedef struct { uint64_t low, high; } vec128_t;
    const vec128_t *src = (const vec128_t*) src32;
    vec128_t *dst = (vec128_t*) dst32;

    // with old gcc, this gets gcc to use a pointer compare as the loop condition
    // instead of incrementing a loop counter
    const vec128_t *src_endp = src + len/(sizeof(vec128_t)/sizeof(uint32_t));
    // len is in units of 4-byte chunks

    while (src < src_endp) {

        #if defined(__ARM_NEON__) || defined(__ARM_NEON)
          #if __LP64__   // FIXME: doesn't account for ILP32 in 64-bit mode
        // aarch64 registers: s0 and d0 are subsets of q0 (128bit), synonym for v0
        asm ("ldr        q0, %[src] \n\t"
             "rev32.16b  v0, v0 \n\t"
             "str        q0, %[dst]  \n\t"
                     : [dst] "=<>m"(*dst)  // auto-increment/decrement or "normal" memory operand
                     : [src] "<>m" (*src)
                     : "q0", "v0"
                     );
          #else
        // arm32 registers: 128bit q0 is made of d0:d1, or s0:s3
        asm ("vld1.32   {d0, d1}, %[src] \n\t"
             "vrev32.8   q0, q0          \n\t"  // reverse 8 bit elements inside 32bit words
             "vst1.32   {d0, d1}, %[dst] \n"
                     : [dst] "=<>m"(*dst)
                     : [src] "<>m"(*src)
                     : "d0", "d1"
                     );
          #endif
        #else
         #error "no NEON"
        #endif

      // increment pointers by 16 bytes
        src++;   // The inline asm doesn't modify the pointers.
        dst++;   // of course, these increments may compile to a post-increment addressing mode
                 // this way has the advantage of letting the compiler unroll or whatever

     }
}

这可以编译(在 Godbolt compiler explorer with gcc 4.8 上),但我不知道它是否可以汇编,更不用说是否正确工作了。不过,我相信这些操作数约束是正确的。所有架构的约束基本相同,我对它们的了解比我对 NEON 的了解要多。

无论如何,在 ARM(32 位)上使用 gcc 4.8 -O3,没有 -funroll-loops 的内部循环是:

.L4:
    vld1.32   {d0, d1}, [r1], #16   @ MEM[(const struct vec128_t *)src32_17]
    vrev32.8   q0, q0          
    vst1.32   {d0, d1}, [r0], #16   @ MEM[(struct vec128_t *)dst32_18]

    cmp     r3, r1    @ src_endp, src32
    bhi     .L4       @,

寄存器约束错误

OP 的自我回答中的代码还有另一个错误:输入指针操作数使用单独的 "r" 约束。如果编译器想要保留旧值,并为 src 选择与输出寄存器不同的输入寄存器,这会导致损坏。

如果你想在寄存器中获取指针输入并选择你自己的寻址模式,你可以使用"0"匹配约束,或者你可以使用"+r"读写输出操作数。

您还需要一个 "memory" 破坏或虚拟内存 input/output 操作数(即告诉编译器读取和写入了哪些内存字节,即使您不使用该操作数编号在内联汇编中)。

有关使用 r 约束在 x86 上循环数组的优点和缺点的讨论,请参阅 。 ARM 具有自动递增寻址模式,与手动选择寻址模式相比,它似乎可以生成更好的代码。它允许 gcc 在循环展开时在块的不同副本中使用不同的寻址模式。 "r" (pointer) 约束似乎没有优势,所以我不会详细介绍如何使用虚拟输入/输出约束来避免需要 "memory" 破坏。


使用@bitwise 的 asm 语句生成错误代码的测试用例:

// return a value as a way to tell the compiler it's needed after
uint32_t* unsafe_asm(uint32_t *dst, const uint32_t *src)
{
  uint32_t *orig_dst = dst;

  uint32_t initial_dst0val = orig_dst[0];
#ifdef __ARM_NEON
  #if __LP64__
asm volatile("ldr q0, [%0], #16   # unused src input was %2\n\t"
             "rev32.16b v0, v0   \n\t"
             "str q0, [%1], #16   # unused dst input was %3\n"
             : "=r"(src), "=r"(dst)
             : "r"(src), "r"(dst)
             : "d0", "d1"  // ,"memory"
               // clobbers don't include v0?
            );
  #else
asm volatile("vld1.32 {d0, d1}, [%0]!  # unused src input was %2\n\t"
             "vrev32.8 q0, q0         \n\t"
             "vst1.32 {d0, d1}, [%1]!  # unused dst input was %3\n"
             : "=r"(src), "=r"(dst)
             : "r"(src), "r"(dst)
             : "d0", "d1" // ,"memory"
             );
  #endif
#else
    #error "No NEON/AdvSIMD"
#endif

  uint32_t final_dst0val = orig_dst[0];
  // gcc assumes the asm doesn't change orig_dst[0], so it only does one load (after the asm)
  // and uses it for final and initial
  // uncomment the memory clobber, or use a dummy output operand, to avoid this.
  // pointer + initial+final compiles to LSL 3 to multiply by 8 = 2 * sizeof(uint32_t)


  // using orig_dst after the inline asm makes the compiler choose different registers for the
  // "=r"(dst) output operand and the "r"(dst) input operand, since the asm constraints
  // advertise this non-destructive capability.
  return orig_dst + final_dst0val + initial_dst0val;
}

编译为 (AArch64 gcc4.8 -O3):

    ldr q0, [x1], #16   # unused src input was x1   // src, src
    rev32.16b v0, v0   
    str q0, [x2], #16   # unused dst input was x0   // dst, dst

    ldr     w1, [x0]  // D.2576, *dst_1(D)
    add     x0, x0, x1, lsl 3 //, dst, D.2576,
    ret

存储使用 x2(一个未初始化的寄存器,因为这个函数只需要 2 个参数)。 "=r"(dst) 输出 (%1) 选择了 x2,而 "r"(dst) 输入(仅在注释中使用的 %3)选择了 x0.

final_dst0val + initial_dst0val 编译为 2x final_dst0val,因为我们欺骗了编译器并告诉它内存没有被修改。因此,它不是在内联 asm 语句前后读取相同的内存,而是在添加到指针时读取之后并左移一个额外的位置。 (return 值的存在只是为了使用这些值,因此它们不会被优化掉)。

我们可以通过更正约束来解决这两个问题:对指针使用 "+r" 并添加 "memory" 破坏。 (虚拟输出也可以,并且可能对优化的影响较小。)我没有打扰,因为这似乎比上面的内存操作数版本没有优势。

通过这些更改,我们得到

safe_register_pointer_asm:
    ldr     w3, [x0]  //, *dst_1(D)
    mov     x2, x0    // dst, dst    ### These 2 insns are new

    ldr q0, [x1], #16       // src
    rev32.16b v0, v0   
    str q0, [x2], #16       // dst

    ldr     w1, [x0]  // D.2597, *dst_1(D)
    add     x3, x1, x3, uxtw  // D.2597, D.2597, initial_dst0val   ## And this is new, to add the before and after loads
    add     x0, x0, x3, lsl 2 //, dst, D.2597,
    ret