将内联汇编与 C 代码混合 - 如何保护寄存器并最小化内存访问

Mar*_*rty 4 gcc x86-64 gnu-assembler inline-assembly

我有一个例程,主要想用汇编语言编写,但我需要调用 C 函数来获取处理所需的一些数据。在某些情况下,我可以预先消化数据并加载带有指向它的指针的寄存器,但在其他情况下,我必须调用完整的函数,因为可能的数据集太大。这些函数无法修改,因为它们是别人的代码,并且其接口需要与其他代码保持相同。其中一些驻留在共享库中,尽管有些是通过头文件的内联函数(我无法更改)。

我可以使用 asm 构造将局部变量分配给寄存器:

register int myReg asm( "%r13" );
Run Code Online (Sandbox Code Playgroud)

恐怕如果我直接在汇编中操作 %r13,调用 C 函数并返回,它将需要从内存中刷新,或者更糟糕的是被完全覆盖。对于某些 ABI,我自己直接推送/弹出寄存器也不安全,对吗?我在 Linux 上使用 x86-64 工作。

我现在正在做的事情似乎正在使用 -g -O0,但我担心当我打开 C 代码的优化时,它将开始触及我希望受到保护的寄存器。

一般来说,我的代码流程如下:

asm( "movq %[src], %%r13" : : [src] "m" (myVariablePointer) : "r13" );

localVariable1 = callSomeCfunction( stuff );

storageLocation = index * sizeof( longTermStorageItem );
longTermStorage[storageLocation] = localVariable1;
// some intermediate registers need to be used here for dereferences and math

switch ( localVariable1 )
{
   case CONSTANT_VAL_A:
     C_MACRO_LOOP_CONSTRUCT
     {
       asm( "movdqa (%r13), %xmm0\n"
            // ... do some more stuff
     } C_MACRO_LOOP_CONSTRUCT_ENDING
   break;
   case CONSTANT_VAL_B:
     // ... and so forth
}
Run Code Online (Sandbox Code Playgroud)

“C_MACRO_LOOP_CONSTRUCT”是来自带有“for”循环的外部头文件的#defines,需要在进程中取消引用一些指针和其他内容,并将迭代器存储在局部变量中。

所以我关心的是如何确保 %r13 在所有这些东西中得到保留。到目前为止,编译器还没有触及它,但我确信这更多的是运气而不是设计。价值的保存本身并不是我唯一关心的问题。如果可能的话,我希望它保留在我放置它的寄存器中。频繁地将其移出到本地/堆栈存储并返回会降低我的性能。

有没有办法可以更好地保护一小部分寄存器免受编译器/优化器的影响?

附加信息

这就是我想这样做的原因。看下面的代码:

#include <emmintrin.h>
#include <stdio.h>

__m128d buffer[100];   

int main( void )
{
  unsigned long long *valPtr;

  register __m128d val;
  register __m128d *regPtr;
#ifdef FORCED  
  asm( "movq %[src], %%r13" :
       :
       [src] "r"  (buffer) );
  asm( "pcmpeqd %[src], %[dst]" :
       [dst] "=x" (val) :
       [src] "x" (val) );
  asm( "movdqa %[src], (%%r13)" : :
       [src] "x" (val) );
  asm( "movdqa %[src], 16(%%r13)" : :
       [src] "x" (val) );   
#else
  asm( "pcmpeqd %[src], %[dst]" :
       [dst] "=x" (val) :
       [src] "x" (val) );
  asm( "movdqa %[src], %[dst]" :
       [dst] "=X" (buffer) :
       [src] "x" (val) );
  asm( "movdqa %[src], %[dst]" :
       [dst] "=X" (buffer+1) :
       [src] "x" (val) );
#endif

  valPtr = (unsigned long long *)buffer;
  printf( "OUTPUT: [0] %016llx%016llx, [1] %016llx%016llx\n",
   valPtr[0], valPtr[1], valPtr[2], valPtr[3] );

  return 0;
}
Run Code Online (Sandbox Code Playgroud)

如果我用定义的“FORCED”来编译它,它就会构建并且可以工作。但这很可怕,因为在这种情况下编译器没有保护“%r13”(它可以是任何寄存器,无关紧要)。但是通过使用硬编码寄存器,我可以使用索引寻址模式,即16(%%r13). 这为我节省了增加值的额外指令,并让我一步即可将其存储到新位置。

如果我尝试在没有“FORCED”的情况下进行编译,gcc 会报告:

y.c: In function \u2018main\u2019:
y.c:32: error: invalid lvalue in asm statement
y.c:30: error: invalid lvalue in asm output 0
Run Code Online (Sandbox Code Playgroud)

所以我想我的问题应该变成,我可以使用带有适当约束的索引寻址模式吗?我尝试了“m”、“X”和“o”。没有不同。如果我尝试将偏移量拉入装配体并拉出参数,如下所示:

asm( "movdqa %[src], 16(%[dst])" :
 [dst] "=m" (buffer) :
 [src] "x" (val) );
Run Code Online (Sandbox Code Playgroud)

GCC 回应:

/tmp/ccoNwyco.s: Assembler messages:
/tmp/ccoNwyco.s:28: Error: junk `(buffer(%rip))' after expression
Run Code Online (Sandbox Code Playgroud)

知道如何使用这种寻址模式并消除不必要的指令吗?

Dav*_*erd 5

既然你问了附加部分,我就集中讨论一下。查看您的第一个 #if 块:

__m128d buffer[100];   

int main( void )
{
  register __m128d val;

  asm( "movq %[src], %%r13" :
       :
       [src] "r"  (buffer) );
  asm( "pcmpeqd %[src], %[dst]" :
       [dst] "=x" (val) :
       [src] "x" (val) );
  asm( "movdqa %[src], (%%r13)" : :
       [src] "x" (val) );
  asm( "movdqa %[src], 16(%%r13)" : :
       [src] "x" (val) );   
}
Run Code Online (Sandbox Code Playgroud)

该片段写入 r13,而不告诉编译器。那是非常糟糕的。即使在调用此 asm 之前您在某个局部变量上有一个 asm("r13") ,这也会很糟糕。您仍然需要将该局部变量列为输出,然后列为后续 asms 的输入。更重要的是,这会让维护者感到困惑,而且没有必要。

另外,像这样使用多个 asm 语句也是一个坏主意。gcc 可能不会选择按此顺序保留它们。既然如此,我建议更像这样:

__m128d buffer[100];   

int main( void )
{
  register __m128d val;

  asm("# val: %0" : "=x" (val)); /* fix "is used uninitialized" warning */

  asm( "pcmpeqd %[sval], %[dval]\n\t"
       "movdqa %[dval], %[buffer]\n\t"
       "movdqa %[dval], %[buffer1]" :

       [dval] "=x" (val), [buffer] "=m" (buffer[0]), [buffer1] "=m" (buffer[1]) :
       [sval] "x" (val) );
}
Run Code Online (Sandbox Code Playgroud)

至于你的#else块:

__m128d buffer[100];   

int main( void )
{
  register __m128d val;

  asm( "pcmpeqd %[src], %[dst]" :
       [dst] "=x" (val) :
       [src] "x" (val) );
  asm( "movdqa %[src], %[dst]" :
       [dst] "=X" (buffer) :
       [src] "x" (val) );
  asm( "movdqa %[src], %[dst]" :
       [dst] "=X" (buffer+1) :
       [src] "x" (val) );
}
Run Code Online (Sandbox Code Playgroud)

我会建议:

__m128d buffer[100];   

int main( void )
{
  register __m128d val;

  asm("# val: %0" : "=x" (val)); /* fix "is used uninitialized" warning */

  asm( "pcmpeqd %[sval], %[dval]\n\t"
       "movdqa %[dval], (%[sbuffer])\n\t"
       "movdqa %[dval], 16(%[sbuffer])" :

       [dval] "=x" (val), [buffer] "=m"  (buffer), [buffer1] "=m" (buffer[1]) :
       [sval] "x" (val), [sbuffer] "r"  (buffer));
}
Run Code Online (Sandbox Code Playgroud)

这里有几点需要注意。

  1. 我正在使用第一个 asm 语句来解决有关在分配之前使用 val 的编译器警告。这是由于使用 val 作为输入而导致的,而它从未被分配过值。大概在您的实际代码中,您在使用它之前分配了一个合理的值。
  2. 通过将 3 个 asm 语句放入一个 asm 块中,gcc 无法移动各个片段。
  3. 为什么我有 sbuffer、buffer 和 buffer1,但从来没有引用 buffer 和 buffer1?sbuffer 用于将指向数组的指针放入寄存器中。“buffer”和“buffer1”被列为输出,因为我必须告诉 gcc 我正在更改它们。使用“内存”破坏器更容易,但这可能会产生严重的性能影响。或者,我可以使用某种形式(来自 gcc 文档重新扩展的 asm):

{“m”( ({ struct { char x[10]; } *p = (void *)ptr ; *p; }) )}。

这告诉 gcc 将访问从 ptr 开始的 10 个字符。丑陋,但如果您在编译时知道要修改多少字节的内存,它就可以工作。重点是,如果您要更改 asm 中的任何值(甚至是数组中的条目),您必须让 gcc 知道。

还有什么?啊,是的,让我们看看 asm(来自 -Os):

pcmpeqd %xmm0, %xmm0
movdqa %xmm0, (%rax)
movdqa %xmm0, 16(%rax)
Run Code Online (Sandbox Code Playgroud)

据我了解,您尝试使用 r13 的全部原因是为了避免当您调用一些您无法控制的子例程时寄存器被破坏,从而浪费每个循环重新加载它的周期。所以让这段代码使用 rax,好吧,这似乎不是一个好主意,对吧?可是等等!观察这段代码会发生什么:

__m128d buffer[100];   

int main( void )
{
  register __m128d val;

  for (int x=0; x < 10; x++)
  {
    asm("# val: %0" : "=x" (val)); /* fix "is used uninitialized" */

    asm( "pcmpeqd %[src], %[dst]\n\t"
         "movdqa %[src], (%[sbuffer])\n\t" /* buffer[0] */
         "movdqa %[src], 16(%[sbuffer])" : /* buffer[1] */

         [dst] "=x" (val), [buffer] "=m"  (buffer), [buffer1] "=m" (buffer[1]) :
         [src] "x" (val), [sbuffer] "r"  (buffer));

     printf("%d\n", val);
   }
}
Run Code Online (Sandbox Code Playgroud)

asm 是相同的,但现在我们处于循环中并调用 printf(我们无法控制的例程)。现在asm是什么样子的?这是循环:

.L2:
    leaq    .LC0(%rip), %rcx
    movq    %rdi, %rdx
    pcmpeqd %xmm6, %xmm0
    movdqa %xmm6, (%rbx)
    movdqa %xmm6, 16(%rbx)
    movapd  %xmm0, 32(%rsp)
    call    printf
    subl    $1, %esi
    jne .L2
Run Code Online (Sandbox Code Playgroud)

嗯,它已经从 rax 变成了 rbx。那个更好吗?嗯,事实上确实如此。当您在 c 中调用子例程时,编译器必须遵循一些规则 (ABI)。这些规则控制诸如参数传递的位置、返回值位于何处、谁清理堆栈以及(对于我们的目的来说最重要的)子例程必须保留的寄存器(即返回时必须具有相同的值)之类的事情。维基百科上有一些关于此的讨论和有用的链接。值得注意的一件事是必须保留 rbx(对于 x86-64)。

因此,如果您查看此代码周围的 asm,您会注意到 rbx 仅加载一次(在循环之外)。Gcc 知道,如果任何子例程与 rbx 发生冲突,它们会在完成后将其放回去。更重要的是,由于子例程知道它们必须保留 rbx,因此它们倾向于避免它,除非多一个可用寄存器的好处大于保存/恢复它的成本。

至于“保留”寄存器并阻止任何子例程使用它的整个想法,我不会说这是不可能的(请参阅Global Reg Vars-ffixed-reg),但我会说这通常是一个糟糕的想法。x86 上的寄存器是非常有用但非常有限的资源。尝试限制可用数量几乎肯定会导致比它所能解决的更多的性能问题。

这里有两个重要的要点:

  1. 相信编译器。让它知道您需要一个指向寄存器中“缓冲区”的指针通常就足够了。Gcc(通常)足够聪明,可以为任务选择最佳寄存器。
  2. 您必须通过使用 clobbers 或输出(不要修改输入)告诉 gcc 您在 asm 块中更改的所有内容。如果不这样做将会导致奇怪且难以追踪的问题。

好的,这里有很多细节(可能比您需要的多得多)。希望您寻求的答案也在这里。