当前位置:  开发笔记 > 编程语言 > 正文

使用内联汇编在数组上循环

如何解决《使用内联汇编在数组上循环》经验,为你挑选了1个好方法。

当使用内联汇编循环数组时,我应该使用寄存器修饰符"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需求值都需要出现在寄存器中。


这是我的版本,注释中有一些调整。

#include 
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

为此以及以下几个版本的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约束的版本

#include 
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

使用[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需求值都需要出现在寄存器中。


这是我的版本,注释中有一些调整。

#include 
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

为此以及以下几个版本的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约束的版本

#include 
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

使用[yi]+x输入/输出操作数会更简单,但是写这种方式使得在联汇编取消对负载,而不是让编译器得到一个值转换成我们的寄存器变化较小。

推荐阅读
小色米虫_524
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有