RGBA到ABGR:iOS / Xcode的内联手臂霓虹asm

时间:2016-06-26 05:00:14

标签: ios xcode assembly arm neon

此代码(非常相似的代码,尚未尝试完全此代码)使用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

此代码有什么问题?

EDIT1:

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

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

3 个答案:

答案 0 :(得分:3)

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

1)使用NEON内在函数编写代码。 ARM指定原始ARM32内在函数对于ARMv8目标基本保持不变,因此可以编译为ARM32和ARM64代码。这是最安全/最简单的选择。

2)编写内联代码或单独的' .S'汇编语言代码的模块。要处理2种编译模式,请使用" #ifdef __arm64 __"和#34; #ifdef __arm __"区分两个指令集。

答案 1 :(得分:3)

Intrinsics显然是在ARM(32位)和AArch64之间使用相同代码进行NEON的唯一方法。

有很多理由不使用https://gcc.gnu.org/wiki/DontUseInlineAsm

不幸的是,目前的编译器通常使用ARM / AArch64内在函数做得很差,这是令人惊讶的,因为他们在优化x86 SSE / AVX内在函数和PowerPC Altivec方面做得非常出色。他们经常做得很好简单的情况,但可以很容易地引入额外的存储/重新加载。

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

  

ARM's official documentation

     

虽然技术上可以手动优化NEON组件,但这可能非常困难,因为管道和内存访问时序具有复杂的相互依赖性。 ARM强烈建议使用内在函数

,而不是手工组装

如果您确实使用内联asm,请通过正确使用来避免将来的痛苦。

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

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

问题中的内联asm是不安全的,因为它会修改内存告诉编译器。这可能只表现在一个循环中,它在内联asm之前和之后从C中的dst读取。但是,它很容易修复,这样做让我们放弃volatile(以及它缺少的'"内存" clobber),这样编译器就可以更好地优化(但与内在函数相比仍有明显的局限性。)

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

以下为ARM和AArch64编译(如果在AArch64上编译ILP32,它可能会失败,但是,我忘记了这种可能性)。使用-funroll-loops会导致gcc选择不同的寻址模式,并且 not 强制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要好得多。

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

.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" clobber或虚拟内存输入/输出操作数(即告诉编译器读取和写入哪些内存字节,即使您不使用该操作数编号) inline asm)。

有关使用r约束在x86上循环数组的优缺点的讨论,请参阅Looping over arrays with inline assembly。 ARM具有自动递增寻址模式,这些模式似乎比手动选择寻址模式所能获得的代码更好。它允许gcc在循环展开时在块的不同副本中使用不同的寻址模式。 "r" (pointer)约束似乎没有优势,因此我不会详细了解如何使用虚拟输入/输出约束来避免需要"memory" clobber。

使用@bitwise&#39; sm语句生成错误代码的测试用例:

// 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语句之前和之后读取相同的内存,而是在添加到指针时只读取并向左移位一个额外的位置。 (返回值仅用于使用值,因此它们不会被优化掉)。

我们可以通过更正约束来解决这两个问题:使用"+r"作为指针并添加"memory" clobber。 (虚拟输出也可以工作,可能会减少优化。)我没有打扰,因为这似乎没有优于上面的内存操作数版本。

通过这些更改,我们得到了

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

答案 2 :(得分:-1)

正如原始问题的编辑中所述,事实证明我需要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

我在原帖中发布的内在函数代码虽然产生了令人惊讶的好组装,但也为我生成了arm64版本,因此将来使用内在函数可能更好。