Z b*_*son 5 c assembly gcc inline-assembly
当使用内联汇编循环数组时,我应该使用寄存器修饰符"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);
}
Run Code Online (Sandbox Code Playgroud)
这是我使用寄存器修饰符"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)
:
);
}
}
Run Code Online (Sandbox Code Playgroud)
这会产生与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])
:
);
}
}
Run Code Online (Sandbox Code Playgroud)
这样效率较低,因为它不使用索引寄存器,而是必须将16添加到每个数组的基址寄存器中.生成的程序集是(gcc(Ubuntu 5.2.1-22ubuntu2)with 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
Run Code Online (Sandbox Code Playgroud)
使用内存修饰符"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("");
}
Run Code Online (Sandbox Code Playgroud)
尽可能避免内联asm:https : //gcc.gnu.org/wiki/DontUseInlineAsm。它阻止了许多优化。但是,如果您真的不能让编译器完成所需的asm,则可能应该在asm中编写整个循环,以便您可以手动展开和调整它,而不是像这样做。
您可以r为索引使用约束。使用q修饰符可获取64位寄存器的名称,因此可以在寻址模式下使用它。当针对32位目标进行编译时,q修饰符选择32位寄存器的名称,因此相同的代码仍然有效。
如果要选择使用哪种寻址方式,则需要自己使用带有r约束的指针操作数来完成。
GNU C内联asm语法不假定您读写指针操作数所指向的内存。(例如,也许您在and指针值上使用inline-asm )。因此,您需要对数据"memory"缓冲区或内存输入/输出操作数进行操作,以使其了解要修改的内存。一"memory"撞是容易的,但一切的力量,除了当地人溅到/重新加载。有关使用伪输入操作数的示例,请参见文档中的Clobbers部分。
具体来说,a "m" (*(const float (*)[]) fptr)将告诉编译器整个数组对象是一个输入,任意长度。也就是说,asm不能与fptr用作地址一部分(或使用已知指向的数组)的任何商店重新排序。也可以使用"=m"或"+m"约束(const显然没有)。
使用特定的大小,例如"m" (*(const float (*)[4]) fptr),可以告诉编译器您读/不读的内容。(或写)。然后,它可以(如果有其他允许的话)将商店下沉到该asm语句之后的某个以后的元素,并将其与您的内联汇编不读取的任何商店的另一个商店合并(或执行死存储消除)。
m约束的另一个巨大好处是-funroll-loops可以通过生成具有恒定偏移量的地址来工作。我们自己进行寻址可防止编译器每4次迭代或类似的操作进行一次增量,因为每个源级别的i需求值都需要出现在寄存器中。
这是我的版本,注释中有一些调整。
#include <immintrin.h>
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<n; i+=4) {
__asm__ __volatile__ (
"movaps (%[y],%q[idx],4), %[vectmp]\n\t" // q modifier: 64bit version of a GP reg
"addps (%[x],%q[idx],4), %[vectmp]\n\t"
"movaps %[vectmp], (%[z],%q[idx],4)\n\t"
: [vectmp] "=x" (vectmp) // "=m" (z[i]) // gives worse code if the compiler prepares a reg we don't use
: [z] "r" (z), [y] "r" (y), [x] "r" (x),
[idx] "r" (i) // unrolling is impossible this way (without an insn for every increment by 4)
: "memory"
// you can avoid a "memory" clobber with dummy input/output operands
);
}
}
Run Code Online (Sandbox Code Playgroud)
为此以及以下几个版本的Godbolt编译器资源管理器 asm输出。
您的版本需要声明%xmm0为已破坏,否则内联时会很糟糕。我的版本使用一个临时变量作为从未使用过的仅输出操作数。这为编译器提供了完全自由的寄存器分配空间。
如果要避免“内存”破坏,可以使用虚拟内存输入/输出操作数,例如"m" (*(const __m128*)&x[i])告诉编译器函数读取和写入哪个内存。如果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 #,
Run Code Online (Sandbox Code Playgroud)
r8,r9和r10是内联asm块不使用的额外指针。
您可以使用约束来告诉gcc任意长度的整个数组是输入还是输出:"m" (*(const struct {char a; char x[];} *) pStr)来自@David Wohlferd在asm上的答案strlen。由于我们要使用索引寻址模式,因此我们将在寄存器中拥有所有三个数组的基地址,并且这种形式的约束要求基地址作为操作数,而不是指向要操作的当前内存的指针。
这实际上可以在循环内无需任何额外的计数器增量的情况下运行:
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<n; i+=4) {
__asm__ __volatile__ (
"movaps (%[y],%q[idx],4), %[vectmp]\n\t" // q modifier: 64bit version of a GP reg
"addps (%[x],%q[idx],4), %[vectmp]\n\t"
"movaps %[vectmp], (%[z],%q[idx],4)\n\t"
: [vectmp] "=x" (vectmp) // "=m" (z[i]) // gives worse code if the compiler prepares a reg we don't use
, "=m" (*(struct {float a; float x[];} *) z)
: [z] "r" (z), [y] "r" (y), [x] "r" (x),
[idx] "r" (i) // unrolling is impossible this way (without an insn for every increment by 4)
, "m" (*(const struct {float a; float x[];} *) x),
"m" (*(const struct {float a; float x[];} *) y)
);
}
}
Run Code Online (Sandbox Code Playgroud)
这为我们提供了与"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 #,
Run Code Online (Sandbox Code Playgroud)
它告诉编译器每个asm块都读取或写入整个数组,因此可能不必要地阻止它与其他代码进行交织(例如,以较低的迭代次数完全展开后)。它不会停止展开,但是要求在寄存器中具有每个索引值的确会使它的有效性降低。
gcc可以展开的有m约束的版本:
#include <immintrin.h>
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<n; i+=4) {
__asm__ __volatile__ (
// "movaps %[yi], %[vectmp]\n\t"
"addps %[xi], %[vectmp]\n\t" // We requested that the %[yi] input be in the same register as the [vectmp] dummy output
"movaps %[vectmp], %[zi]\n\t"
// ugly ugly type-punning casts; __m128 is a may_alias type so it's safe.
: [vectmp] "=x" (vectmp), [zi] "=m" (*(__m128*)&z[i])
: [yi] "0" (*(__m128*)&y[i]) // or [yi] "xm" (*(__m128*)&y[i]), and uncomment the movaps load
, [xi] "xm" (*(__m128*)&x[i])
: // memory clobber not needed
);
}
}
Run Code Online (Sandbox Code Playgroud)
使用[yi]的+x输入/输出操作数会更简单,但是写这种方式使得在联汇编取消对负载,而不是让编译器得到一个值转换成我们的寄存器变化较小。