使用内联汇编在数组上循环
当使用内联汇编循环数组时,我应该使用寄存器修饰符“r”还是内存修饰符“m”?
让我们考虑一个添加两个浮点数组x
和y
并将结果写入z
的示例。 通常我会使用内在函数这样做
for(int i=0; i<n/4; i++) { __m128 x4 = _mm_load_ps(&x[4*i]); __m128 y4 = _mm_load_ps(&y[4*i]); __m128 s = _mm_add_ps(x4,y4); _mm_store_ps(&z[4*i], s); }
这是我使用寄存器修饰符“r”提出的内联汇编解决方案
void add_asm1(float *x, float *y, float *z, unsigned n) { for(int i=0; i<n; i+=4) { __asm__ __volatile__ ( "movaps (%1,%%rax,4), %%xmm0\n" "addps (%2,%%rax,4), %%xmm0\n" "movaps %%xmm0, (%0,%%rax,4)\n" : : "r" (z), "r" (y), "r" (x), "a" (i) : ); } }
这会产生与GCC类似的组装。 主要区别在于GCC将16添加到索引寄存器并使用1的标度,而内联汇编解决方案将4添加到索引寄存器并使用4的标度。
我无法使用通用寄存器作为迭代器。 我必须指定一个在这种情况下是rax
。 是否有一个原因?
这是我想出的使用内存修饰符“m”的解决方案
void add_asm2(float *x, float *y, float *z, unsigned n) { for(int i=0; i<n; i+=4) { __asm__ __volatile__ ( "movaps %1, %%xmm0\n" "addps %2, %%xmm0\n" "movaps %%xmm0, %0\n" : "=m" (z[i]) : "m" (y[i]), "m" (x[i]) : ); } }
这样效率较低,因为它不使用索引寄存器,而是必须将16添加到每个数组的基址寄存器中。 生成的程序集是(gcc(Ubuntu 5.2.1-22ubuntu2),带有gcc -O3 -S asmtest.c
):
.L22 movaps (%rsi), %xmm0 addps (%rdi), %xmm0 movaps %xmm0, (%rdx) addl $4, %eax addq $16, %rdx addq $16, %rsi addq $16, %rdi cmpl %eax, %ecx ja .L22
使用内存修饰符“m”有更好的解决方案吗? 有没有办法让它使用索引寄存器? 我问的原因是,由于我正在阅读和编写内存,因此使用内存修饰符“m”似乎更合乎逻辑。 另外,使用寄存器修饰符“r”我从不使用输出操作数列表,这对我来说似乎很奇怪。
也许有比使用“r”或“m”更好的解决方案?
这是我用来测试它的完整代码
#include #include #define N 64 void add_intrin(float *x, float *y, float *z, unsigned n) { for(int i=0; i<n; i+=4) { __m128 x4 = _mm_load_ps(&x[i]); __m128 y4 = _mm_load_ps(&y[i]); __m128 s = _mm_add_ps(x4,y4); _mm_store_ps(&z[i], s); } } void add_intrin2(float *x, float *y, float *z, unsigned n) { for(int i=0; i<n/4; i++) { __m128 x4 = _mm_load_ps(&x[4*i]); __m128 y4 = _mm_load_ps(&y[4*i]); __m128 s = _mm_add_ps(x4,y4); _mm_store_ps(&z[4*i], s); } } void add_asm1(float *x, float *y, float *z, unsigned n) { for(int i=0; i<n; i+=4) { __asm__ __volatile__ ( "movaps (%1,%%rax,4), %%xmm0\n" "addps (%2,%%rax,4), %%xmm0\n" "movaps %%xmm0, (%0,%%rax,4)\n" : : "r" (z), "r" (y), "r" (x), "a" (i) : ); } } void add_asm2(float *x, float *y, float *z, unsigned n) { for(int i=0; i<n; i+=4) { __asm__ __volatile__ ( "movaps %1, %%xmm0\n" "addps %2, %%xmm0\n" "movaps %%xmm0, %0\n" : "=m" (z[i]) : "m" (y[i]), "m" (x[i]) : ); } } int main(void) { float x[N], y[N], z1[N], z2[N], z3[N]; for(int i=0; i<N; i++) x[i] = 1.0f, y[i] = 2.0f; add_intrin2(x,y,z1,N); add_asm1(x,y,z2,N); add_asm2(x,y,z3,N); for(int i=0; i<N; i++) printf("%.0f ", z1[i]); puts(""); for(int i=0; i<N; i++) printf("%.0f ", z2[i]); puts(""); for(int i=0; i<N; i++) printf("%.0f ", z3[i]); puts(""); }
尽可能避免使用内联asm: https : //gcc.gnu.org/wiki/DontUseInlineAsm 。 它阻止了许多优化。 但是如果你真的不能手持编译器来制作你想要的asm,你应该在asm中编写你的整个循环,这样你就可以手动展开和调整它,而不是像这样做。
您可以对索引使用r
约束。 使用q
修饰符获取64位寄存器的名称,以便在寻址模式下使用它。 当编译为32位目标时, q
修饰符选择32位寄存器的名称,因此相同的代码仍然有效。
如果要选择使用何种寻址模式,则需要使用具有r
约束的指针操作数自行完成。
GNU C inline asm语法不假定您读取或写入指针操作数指向的内存。 (例如,您可能正在使用inline-asm and
指针值)。 所以你需要用"memory"
clobber或内存输入/输出操作数来做一些事情,让它知道你修改了什么内存。 "memory"
破坏很容易,但强制除了当地人以外的所有东西都要溢出/重新加载。 有关使用虚拟输入操作数的示例,请参阅文档中的Clobbers部分 。
m
约束的另一个巨大好处是-funroll-loops
可以通过生成具有常量偏移的地址来工作 。 自己进行寻址可以防止编译器每4次迭代执行一次增量,因为i
每个源级值都需要出现在寄存器中。
这是我的版本,评论中提到了一些调整。
#include void add_asm1_memclobber(float *x, float *y, float *z, unsigned n) { __m128 vectmp; // let the compiler choose a scratch register for(int i=0; i
Godbolt编译器探测器 asm输出为此以及下面的几个版本。
您的版本需要声明%xmm0
为clobbered,否则%xmm0
联时您会遇到错误的时间。 我的版本使用临时变量作为从不使用的仅输出操作数。 这为编译器提供了完全自由的寄存器分配。
如果你想避免“内存”崩溃,你可以使用虚拟内存输入/输出操作数,如"m" (*(const __m128*)&x[i])
来告诉编译器你的函数读取和写入哪些内存。 如果您执行类似x[4] = 1.0;
这对于确保正确的代码生成是必要的x[4] = 1.0;
在运行该循环之前。 (即使你没有写一些简单的东西,内联和常量传播也可以归结为它。)并且还要确保编译器在循环运行之前不从z[]
读取。
在这种情况下,我们得到了可怕的结果:gcc5.x实际上增加了3个额外的指针,因为它决定使用[reg]
寻址模式而不是索引。 它不知道内联asm从未使用约束创建的寻址模式实际引用那些内存操作数!
# gcc5.4 with dummy constraints like "=m" (*(__m128*)&z[i]) instead of "memory" clobber .L11: movaps (%rsi,%rax,4), %xmm0 # y, i, vectmp addps (%rdi,%rax,4), %xmm0 # x, i, vectmp movaps %xmm0, (%rdx,%rax,4) # vectmp, z, i addl $4, %eax #, i addq $16, %r10 #, ivtmp.19 addq $16, %r9 #, ivtmp.21 addq $16, %r8 #, ivtmp.22 cmpl %eax, %ecx # i, n ja .L11 #,
r8,r9和r10是内联asm块不使用的额外指针。
你可以使用一个约束告诉gcc一个任意长度的整个数组是一个输入或输出: "m" (*(const struct {char a; char x[];} *) pStr)
来自@David Wohlferd的答案asm strlen
。 由于我们想要使用索引寻址模式,我们将在寄存器中具有所有三个数组的基地址,并且这种约束forms要求将基地址作为操作数,而不是指向当前正在操作的存储器的指针。
这实际上在循环内没有任何额外的计数器增量工作:
void add_asm1_dummy_whole_array(const float *restrict x, const float *restrict y, float *restrict z, unsigned n) { __m128 vectmp; // let the compiler choose a scratch register for(int i=0; i
这给了我们与"memory"
clobber相同的内循环:
.L19: # with clobbers like "m" (*(const struct {float a; float x[];} *) y) movaps (%rsi,%rax,4), %xmm0 # y, i, vectmp addps (%rdi,%rax,4), %xmm0 # x, i, vectmp movaps %xmm0, (%rdx,%rax,4) # vectmp, z, i addl $4, %eax #, i cmpl %eax, %ecx # i, n ja .L19 #,
它告诉编译器每个asm块读取或写入整个数组,因此可能会不必要地阻止它与其他代码交错(例如,在完全展开后使用低迭代次数)。 它不会停止展开,但要求在寄存器中包含每个索引值确实会降低其效率。
具有m
约束的版本,gcc可以展开 :
#include void add_asm1(float *x, float *y, float *z, unsigned n) { __m128 vectmp; // let the compiler choose a scratch register for(int i=0; i
使用[yi]
作为+x
输入/输出操作数会更简单,但是以这种方式编写它会在内联asm中取消注释负载时产生较小的更改,而不是让编译器为我们获取一个值到寄存器中。
当我使用gcc(4.9.2)编译你的add_asm2代码时,我得到:
add_asm2: .LFB0: .cfi_startproc xorl %eax, %eax xorl %r8d, %r8d testl %ecx, %ecx je .L1 .p2align 4,,10 .p2align 3 .L5: #APP # 3 "add_asm2.c" 1 movaps (%rsi,%rax), %xmm0 addps (%rdi,%rax), %xmm0 movaps %xmm0, (%rdx,%rax) # 0 "" 2 #NO_APP addl $4, %r8d addq $16, %rax cmpl %r8d, %ecx ja .L5 .L1: rep; ret .cfi_endproc
所以它不完美(它使用冗余寄存器),但确实使用索引加载……
gcc
还有内置的矢量扩展 ,甚至是跨平台的:
typedef float v4sf __attribute__((vector_size(16))); void add_vector(float *x, float *y, float *z, unsigned n) { for(int i=0; i
在我的gcc版本4.7.2上,生成的程序集是:
.L28: movaps (%rdi,%rax), %xmm0 addps (%rsi,%rax), %xmm0 movaps %xmm0, (%rdx,%rax) addq $16, %rax cmpq %rcx, %rax jne .L28