RGBA到ABGR:iOS / XCode的内联胳膊霓虹灯asm

这个代码(非常相似的代码,没有试过这个代码)使用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:

通过反汇编阅读,我发现了第二个版本的function。 事实certificate,arm64使用一个稍微不同的指令集。 例如,arm64程序集使用rev32.16b v0, v0代替。 下面是整个function列表(我无法正面或者反面):

 _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 

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

1)使用NEON内部函数编写代码。 ARM规定,对于ARMv8目标,原始的ARM32内部函数将保持大部分不变,因此可以编译为ARM32和ARM64代码。 这是最安全/最简单的select。

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

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

有很多原因不使用内联汇编 : https : //gcc.gnu.org/wiki/DontUseInlineAsm

内在因素也是最好的方法。 你应该得到良好的输出,并且它可以让编译器调度vectoroad和store之间的指令,这对最有用的内核是最有帮助的。 (或者你可以用inline asm编写一个完整的循环,你可以手动安排。)

ARM官方文档 :

尽pipe手工优化NEON组件在技术上是可行的,但是这可能是非常困难的,因为stream水线和存储器访问时序具有复杂的相互依赖关系。 ARM强烈build议使用内部函数,而不是手工组装


如果你真的用inline asm,通过正确的做法来避免未来的痛苦。

写内联asm很容易,但是不安全。 未来的源代码更改(有时甚至是将来的编译器优化),因为这些约束不能准确地描述asm的function。 这些症状会很奇怪,这种上下文敏感的错误甚至会导致主程序中的unit testing通过,但代码错误。 (或相反亦然)。

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

问题中的内联asm是不安全的,因为它修改了内存告诉编译器 。 这可能只是dst联asm之前和之后的一个从C中的dst读取的循环中体现出来。 然而,修复起来很容易,而且这样做可以让我们放弃volatile (以及缺less的“内存”clobber),这样编译器就可以更好地进行优化(但与内部函数相比仍然有很大的局限性)。

volatile 阻止相对于内存访问的重新sorting ,所以它可能不会发生在相当人为的情况之外。 但是这很难certificate。


以下为ARM和Aarch64编译。 使用-funroll-loops会导致gccselect不同的寻址模式,而不是强制dst++; src++; dst++; src++; 发生在每个内联汇编语句之间。 (这可能不会使用asm volatile )。

我使用了内存操作数,因此编译器知道内存是一个input和一个输出,并且给编译器提供使用自动递增/递减寻址模式的选项 。 这比用寄存器中的指针作为input操作数所做的任何事情都好,因为它允许循环展开工作。

这仍然不会让编译器在相应的负载到软件stream水线之后调度存储多条指令的循环 ,所以它可能只会在无序的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__ // 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编译器资源pipe理器与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的自我回答中的代码有另外一个bug:input指针操作数使用单独的"r"约束。 如果编译器想要保留旧的值,并且select一个与输出寄存器不同的src的input寄存器,就会导致破坏。

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

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

有关使用r约束在x86上对数组进行循环的优点和缺点的讨论,请参见使用内联程序集循环数组。 ARM具有自动递增的寻址模式,与使用手动方式select寻址模式时可能产生的任何代码相比,这些寻址模式似乎产生更好的代码 它允许gcc在循环展开时在块的不同副本中使用不同的寻址模式。 "r" (pointer)约束似乎没有优势,所以我不会详细讨论如何使用虚拟input/输出约束来避免需要"memory"破坏。


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

 // 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)select了x2 ,而"r"(dst)input(%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 

正如在对原始问题的编辑中所述,事实certificate,我需要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版本,因此将来使用intrinsics可能会更好。