当使用内联汇编循环数组时,我应该使用寄存器修饰符" 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);
}
这是我使用注册修饰符&#34; r&#34;
提出的内联汇编解决方案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
。这有什么理由吗?
这是我使用内存修改器&#34; m&#34;
提出的解决方案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
是否有更好的解决方案使用内存修饰符&#34; m&#34;?有没有办法让它使用索引寄存器?我问的原因是我使用内存修饰符似乎更合乎逻辑&#34; m&#34;因为我正在阅读和写作记忆。另外,使用寄存器修饰符&#34; r&#34;我从不使用输出操作数列表,这对我来说似乎很奇怪。
也许有比使用&#34; r&#34;更好的解决方案。或&#34; m&#34;?
以下是我用来测试这个
的完整代码#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("");
}
答案 0 :(得分:4)
尽可能避免使用内联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"
clobber很容易,但强制除了本地人之外的所有内容都会被溢出/重新加载。有关使用虚拟输入操作数的示例,请参阅Clobbers section in the docs。
具体来说,"m" (*(const float (*)[]) fptr)
会告诉编译器整个数组对象是一个输入,任意长度。即,asm不能对使用fptr
作为地址一部分的任何商店(或者使用已知的数组)进行重新排序。也适用于"=m"
或"+m"
约束(显然没有const
)。
使用特定大小(如"m" (*(const float (*)[4]) fptr)
)可让您告诉编译器您要执行/不阅读的内容。 (或写)。然后它可以(如果允许的话)将商店下沉到asm
语句之后的后一个元素,并将它与您的任何商店的另一个商店(或去除商店)相结合内联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
);
}
}
Godbolt compiler explorer asm输出以及以下几个版本。
您的版本需要将%xmm0
声明为已破坏,否则在内联时您会遇到错误的时间。我的版本使用临时变量作为从不使用的仅输出操作数。这为编译器提供了完全自由的寄存器分配。
如果你想避免记忆&#34; clobber,您可以使用虚拟内存输入/输出操作数(如"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 #,
r8,r9和r10是内联asm块无法使用的额外指针。
您可以使用约束来告诉gcc任意长度的整个数组是输入或输出:来自@David Wohlferd's answer on an asm strlen
的"m" (*(const struct {char a; char x[];} *) pStr)
。由于我们想要使用索引寻址模式,我们将在寄存器中拥有所有三个数组的基地址,这种约束形式要求将基地址作为操作数,而不是指向当前正在操作的内存的指针。
这实际上在循环内没有任何额外的计数器增量的情况下工作:
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)
);
}
}
这给了我们与"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
约束that gcc can unroll 的版本:
#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
);
}
}
使用[yi]
作为+x
输入/输出操作数会更简单,但是以这种方式编写它会对内联asm中的负载取消注释进行较小的更改,而不是让编译器获得一个值进入我们的登记册。
答案 1 :(得分:2)
当我使用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
所以它不完美(它使用冗余寄存器),但确实使用索引加载......
答案 2 :(得分:2)
gcc
也有builtin vector extensions甚至跨平台:
typedef float v4sf __attribute__((vector_size(16)));
void add_vector(float *x, float *y, float *z, unsigned n) {
for(int i=0; i<n/4; i+=1) {
*(v4sf*)(z + 4*i) = *(v4sf*)(x + 4*i) + *(v4sf*)(y + 4*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