当使用内联汇编循环遍历数组时,我应该使用寄存器修饰符“r”还是内存修饰符“m”?
让我们考虑一个例子,它将两个浮点数数组x和y相加,并将结果写入z。通常我会像这样使用内置函数来完成此操作:
这将生成类似于GCC的汇编代码。主要区别在于,GCC将索引寄存器加上16,并使用比例为1,而内联汇编解决方案将索引寄存器加上4,并使用比例为4。
我无法为迭代器使用通用寄存器。我不得不指定一个寄存器,这种情况下是rax。这是有原因的吗?
这是我使用“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 <stdio.h>
#include <x86intrin.h>
#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("");
}
long long
或使用%q3
来强制使用完整寄存器。顺便说一句,由于add_asm1修改了内存,因此应该使用内存破坏。 - David Wohlferd(%1,%4,4)
而不是(%1,%%rax,4)
,其中%4
是gcc决定的任何寄存器,而不是强制它成为rax
。 - Z boson%3
。我尝试了一下,它可以工作,我甚至不需要切换到long long i=0
。从汇编代码来看,我发现gcc使用%eax
。这是一个更好的解决方案,因为没有理由使用%rax
作为索引。如果你想写一个答案,我会给你点赞。 - Z bosonmemory
clobber:如果你不能告诉编译器哪个内存被破坏了,那么就使用memory
。在这种情况下,它是可预测的,所以你可以使用Clobbers部分末尾建议的语句表达式技巧:{"m"(({ struct { char x[16]; } *p = (void *)(z+i*4) ; *p; }) )}
。我修改了示例以适应您的代码:clobber 16 字节,在&z[i*4]
处。此外,请注意,如果使用一个内存输出操作数,那么您的 asm 上不需要__volatile__
,因为它知道无法将 store 提升到z[i]
。 - Peter Cordes