此代码(非常相似的代码,尚未尝试完全此代码)使用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
答案 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的唯一方法。
有很多理由不使用inline-assembly:https://gcc.gnu.org/wiki/DontUseInlineAsm
不幸的是,目前的编译器通常使用ARM / AArch64内在函数做得很差,这是令人惊讶的,因为他们在优化x86 SSE / AVX内在函数和PowerPC Altivec方面做得非常出色。他们经常做得很好简单的情况,但可以很容易地引入额外的存储/重新加载。
理论上,对于内在函数,你应该获得良好的asm输出,并且它允许编译器在向量加载和存储之间调度指令,这将有助于大多数有序内核。 (或者你可以在手动安排的内联asm中编写一个完整的循环。)
虽然技术上可以手动优化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。
// 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版本,因此将来使用内在函数可能更好。