C 使用内联程序集在阵列上循环
当使用内联汇编在数组上循环时,我应该使用寄存器修饰符“r”还是内存修饰符“m”?C 使用内联程序集在阵列上循环,c,gcc,assembly,inline-assembly,C,Gcc,Assembly,Inline Assembly,当使用内联汇编在数组上循环时,我应该使用寄存器修饰符“r”还是内存修饰符“m”? 让我们考虑一个例子,它添加了两个浮点数组 x>代码>和 y>代码>,并将结果写入 z < /代码>。通常我会使用intrinsics这样做 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
让我们考虑一个例子,它添加了两个浮点数组<代码> x>代码>和<代码> y>代码>,并将结果写入<代码> z < /代码>。通常我会使用intrinsics这样做
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);
}
这是效率较低的,因为它不使用索引寄存器,而是必须向每个数组的基址寄存器添加16。生成的程序集是(gcc(ubuntu5.2.1-22ubuntu2),带有gcc-O3-S asmtest.c
):
使用内存修饰符“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("");
}
#包括
#包括
#定义N 64
void add_intrin(浮点*x,浮点*y,浮点*z,无符号n){
对于(inti=0;i当我用gcc(4.9.2)编译add_asm2代码时,我得到:
因此,它并不完美(它使用冗余寄存器),但确实使用索引加载…gcc
还具有跨平台的功能:
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);
}
}
尽可能避免内联asm:。它会阻止许多优化。但是如果您确实无法手动控制编译器生成所需的asm,您可能应该在asm中编写整个循环,以便手动展开和调整它,而不是执行类似的操作
您可以对索引使用r
约束。使用q
修饰符获取64位寄存器的名称,这样您就可以在寻址模式下使用它。为32位目标编译时,q
修饰符选择32位寄存器的名称,因此相同的代码仍然有效
如果您想选择使用哪种寻址模式,您需要自己选择,使用带有r
约束的指针操作数
GNU C内联asm语法不假定您读取或写入指针操作数指向的内存。(例如,可能您在指针值上使用了内联asm和)。因此,您需要使用“内存”
clobber或内存输入/输出操作数来让它知道您修改了什么内存。“内存”
clobber很容易,但会强制溢出/重新加载除局部数以外的所有内容。有关使用虚拟输入操作数的示例,请参阅
特别是“m”(*(const float(*)[])fptr)
将告诉编译器整个数组对象是一个输入,任意长度。也就是说,asm不能对任何使用fptr
作为地址一部分(或使用已知指向的数组)的存储进行重新排序。也可以使用“=m”
或“+m”
约束(显然没有常量)
使用一个特定的大小,如“m”(*(const float(*)[4])fptr)
可以告诉编译器您读(或写)什么/不读(或写)什么。然后它可以(如果允许的话)将一个存储通过asm
语句放入后面的元素,并将其与另一个存储结合(或执行死存储消除)内联asm未读取的所有存储
(有关这方面的完整问答,请参见。)
使用m
约束的另一个巨大好处是-funroll循环
可以通过生成具有恒定偏移量的地址来工作。自行进行寻址可以防止编译器每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
);
}
}
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" (*(float (*)[]) z) // "=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)
, "m" (*(const float (*)[]) x),
"m" (*(const float (*)[]) y) // pointer to unsized array = all memory from this pointer
);
}
}
r8、r9和r10是内联asm块不使用的额外指针
您可以使用一个约束,告诉gcc任意长度的整个数组是一个输入或输出:“m”(*(const char(*)[])pStr)
。这会将指针强制转换为指向数组的指针(大小未指定)。请参阅
如果我们想使用索引寻址模式,我们将在寄存器中拥有所有三个数组的基址,这种形式的约束要求将基址(整个数组的)作为操作数,而不是指向正在操作的当前内存的指针
这实际上可以在循环中不使用任何额外的指针或计数器增量的情况下工作:(避免使用“内存”
,但编译器仍然无法轻松展开)
它告诉编译器每个asm块读取或写入整个数组,因此可能会不必要地阻止它与其他代码交错(例如,在以低迭代次数完全展开后)。它不会停止展开,但要求在寄存器中包含每个索引值确实会降低其效率。这不可能以16(%rsi,%rax,4)
寻址模式结束,该模式位于同一循环中此块的第二个副本中,因为我们对编译器隐藏了寻址
#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
);
}
}
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" (*(float (*)[]) z) // "=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)
, "m" (*(const float (*)[]) x),
"m" (*(const float (*)[]) y) // pointer to unsized array = all memory from this pointer
);
}
}
具有m
约束的版本,:
#include <immintrin.h>
void add_asm1(float *x, float *y, float *z, unsigned n) {
// x, y, z are assumed to be aligned
__m128 vectmp; // let the compiler choose a scratch register
for(int i=0; i<n; i+=4) {
__asm__ __volatile__ (
// "movaps %[yi], %[vectmp]\n\t" // get the compiler to do this load instead
"addps %[xi], %[vectmp]\n\t"
"movaps %[vectmp], %[zi]\n\t"
// __m128 is a may_alias type so these casts are safe.
: [vectmp] "=x" (vectmp) // let compiler pick a stratch reg
,[zi] "=m" (*(__m128*)&z[i]) // actual memory output for the movaps store
: [yi] "0" (*(__m128*)&y[i]) // or [yi] "xm" (*(__m128*)&y[i]), and uncomment the movaps load
,[xi] "xm" (*(__m128*)&x[i])
//, [idx] "r" (i) // unrolling with this would need an insn for every increment by 4
);
}
}
#包括
void add_asm1(浮点*x,浮点*y,浮点*z,无符号n){
//假设x,y,z是对齐的
__m128 vectmp;//让编译器选择一个暂存寄存器
对于(inti=0;iInteresting,gcc(Ubuntu 5.2.1-22ubuntu2)
不这样做(如果您想查看,我在我的问题中添加了程序集输出)。您的结果与我的add_intrin
函数是相同的程序集。这就是我使用add_intrin2
的原因。它没有使用冗余寄存器。在这种情况下,GCC 5.2.1的效率为什么低于4.9.2?@Zboson:我假设他们在玩的时候改变了一些东西
#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
);
}
}
# 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 #,
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" (*(float (*)[]) z) // "=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)
, "m" (*(const float (*)[]) x),
"m" (*(const float (*)[]) y) // pointer to unsized array = all memory from this pointer
);
}
}
.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 #,
#include <immintrin.h>
void add_asm1(float *x, float *y, float *z, unsigned n) {
// x, y, z are assumed to be aligned
__m128 vectmp; // let the compiler choose a scratch register
for(int i=0; i<n; i+=4) {
__asm__ __volatile__ (
// "movaps %[yi], %[vectmp]\n\t" // get the compiler to do this load instead
"addps %[xi], %[vectmp]\n\t"
"movaps %[vectmp], %[zi]\n\t"
// __m128 is a may_alias type so these casts are safe.
: [vectmp] "=x" (vectmp) // let compiler pick a stratch reg
,[zi] "=m" (*(__m128*)&z[i]) // actual memory output for the movaps store
: [yi] "0" (*(__m128*)&y[i]) // or [yi] "xm" (*(__m128*)&y[i]), and uncomment the movaps load
,[xi] "xm" (*(__m128*)&x[i])
//, [idx] "r" (i) // unrolling with this would need an insn for every increment by 4
);
}
}