gpt4 book ai didi

gcc - 内联汇编与C代码混合-如何保护寄存器并最大程度地减少内存访问

转载 作者:行者123 更新时间:2023-12-02 06:08:48 26 4
gpt4 key购买 nike

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

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

register int myReg asm( "%r13" );


恐怕如果我然后直接在汇编中操作%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
}


“ C_MACRO_LOOP_CONSTRUCT”是来自带有“ for”循环的外部头文件的#define,该循环需要在过程中取消引用某些指针和诸如此类的东西,并将迭代器存储在局部变量中。

所以我关心的是如何确保在所有这些内容中保留%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;
}


如果我使用定义的“ 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


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

asm( "movdqa %[src], 16(%[dst])" :
[dst] "=m" (buffer) :
[src] "x" (val) );


GCC回应:

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


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

最佳答案

由于您询问了其他部分,因此我将集中讨论。查看您的第一个#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) );
}


该片段写入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) );
}


至于您的#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) );
}


我会建议:

__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));
}


这里有几件事要注意。


我正在使用第一个asm语句来解决有关在分配val之前使用val的编译器警告。这是由于在从未为其分配值的情况下使用val作为输入引起的。大概在您的真实代码中,您在使用之前分配了一个合理的值。
通过将3条asm语句放入一个asm块中,gcc不能移动单个块。
为什么我有sbuffer,buffer和buffer1,却从不引用buffer和buffer1? sbuffer用于将指向数组的指针获取到寄存器中。 “ buffer”和“ buffer1”被列为输出,因为我必须告诉gcc我正在更改它们。使用“内存”缓冲区更容易,但是这可能会对性能产生严重影响。或者,我可以使用某种形式的(从gcc docs重新扩展为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)


据我了解,您尝试使用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);
}
}


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


好吧,它已经从rax变为rbx。那个更好吗?好吧,实际上是。当您在c中调用子例程时,编译器必须遵循一些规则(ABI)。这些规则控制着诸如传递参数的位置,返回值的位置,清理堆栈的人员,以及(最重要的是我们这里的目的)子例程必须保留的寄存器(即返回时必须具有相同的值)之类的事情。 wikipedia上对此有一些讨论和有用的链接。需要注意的一件事是,必须保留rbx(对于x86-64)。

结果,如果查看此代码周围的汇编,您会注意到rbx仅被加载一次(在循环外)。 Gcc知道,如果有任何子例程与rbx一起使用,它们将在完成后将其放回原处。此外,由于子例程知道必须保留rbx,因此除非有更多寄存器可用的好处大于保存/恢复它的成本,否则它们倾向于避免使用rbx。

至于“保留”寄存器并阻止任何子例程使用它的整个想法,好吧,我不会说这是不可能的(请参见 Global Reg Vars-ffixed-reg),但是我会说通常这是一个糟糕的主意。 x86上的寄存器是非常有用且非常有限的资源。试图限制可用数量几乎肯定会导致性能问题超过其修复的范围。

这里有两个重要的建议:


信任编译器。让它知道您需要一个指向寄存器中“缓冲区”的指针通常就足够了。 Gcc(通常)足够聪明,可以为任务选择最佳的寄存器。
您必须使用Clobber或输出(请勿修改输入),必须将所有在asm块中更改的内容告知gcc。否则,将导致问题怪异且难以追踪。


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

关于gcc - 内联汇编与C代码混合-如何保护寄存器并最大程度地减少内存访问,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23235261/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com