当使用内联汇编循环数组时,我应该使用寄存器修饰符"r"还是内存修饰符"m"?
让我们考虑其将两个浮标阵为例x
,与y
和结果写入z
.通常我会使用内在函数这样做
for(int i=0; i这是我使用寄存器修饰符"r"提出的内联汇编解决方案
void add_asm1(float *x, float *y, float *z, unsigned n) { for(int i=0; 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这样效率较低,因为它不使用索引寄存器,而是必须将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使用内存修饰符"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
Peter Cordes.. 5
尽可能避免内联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
需求值都需要出现在寄存器中。
这是我的版本,注释中有一些调整。
#includevoid 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
为已破坏,否则内联时会很糟糕。我的版本使用一个临时变量作为从未使用过的仅输出操作数。这为编译器提供了完全自由的寄存器分配空间。如果要避免“内存”破坏,可以使用虚拟内存输入/输出操作数,例如
"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任意长度的整个数组是输入还是输出:
"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这为我们提供了与
"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块都读取或写入整个数组,因此可能不必要地阻止它与其他代码进行交织(例如,以较低的迭代次数完全展开后)。它不会停止展开,但是要求在寄存器中具有每个索引值的确会使它的有效性降低。
gcc可以展开的有
m
约束的版本:#includevoid 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
输入/输出操作数会更简单,但是写这种方式使得在联汇编取消对负载,而不是让编译器得到一个值转换成我们的寄存器变化较小。
1> Peter Cordes..:尽可能避免内联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
需求值都需要出现在寄存器中。
这是我的版本,注释中有一些调整。
#includevoid 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
为已破坏,否则内联时会很糟糕。我的版本使用一个临时变量作为从未使用过的仅输出操作数。这为编译器提供了完全自由的寄存器分配空间。如果要避免“内存”破坏,可以使用虚拟内存输入/输出操作数,例如
"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任意长度的整个数组是输入还是输出:
"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这为我们提供了与
"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块都读取或写入整个数组,因此可能不必要地阻止它与其他代码进行交织(例如,以较低的迭代次数完全展开后)。它不会停止展开,但是要求在寄存器中具有每个索引值的确会使它的有效性降低。
gcc可以展开的有
m
约束的版本:#includevoid 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
输入/输出操作数会更简单,但是写这种方式使得在联汇编取消对负载,而不是让编译器得到一个值转换成我们的寄存器变化较小。