如何指示可以使用内联 ASM 参数*指向*的内存?

2024-01-17

考虑以下小函数:

void foo(int* iptr) {
    iptr[10] = 1;
    __asm__ volatile ("nop"::"r"(iptr):);
    iptr[10] = 2;
}

使用海湾合作委员会,这编译为 https://godbolt.org/z/A3yVqA:

foo:
        nop
        mov     DWORD PTR [rdi+40], 2
        ret

特别注意,首先写入iptr, iptr[10] = 1根本不会发生:内联汇编nop是函数中的第一件事,并且只是最后的写入2出现(在 ASM 调用之后)。显然编译器决定它只需要提供值的最新版本iptr itself,但不是它指向的内存。

我可以告诉编译器内存必须是最新的memory破坏,像这样:

void foo(int* iptr) {
    iptr[10] = 1;
    __asm__ volatile ("nop"::"r"(iptr):"memory");
    iptr[10] = 2;
}

这会产生预期的代码:

foo:
        mov     DWORD PTR [rdi+40], 1
        nop
        mov     DWORD PTR [rdi+40], 2
        ret

然而,这是太强条件,因为它告诉编译器all内存必须被写入。例如,在以下函数中:

void foo2(int* iptr, long* lptr) {
    iptr[10] = 1;
    lptr[20] = 100;
    __asm__ volatile ("nop"::"r"(iptr):);
    iptr[10] = 2;
    lptr[20] = 200;
}

期望的行为是让编译器优化掉第一次写入lptr[20],但不是第一次写入iptr[10]. The "memory"clobber 无法实现此目的,因为这意味着必须同时进行两次写入:

foo2:
        mov     DWORD PTR [rdi+40], 1
        mov     QWORD PTR [rsi+160], 100 ; lptr[10] written unecessarily
        nop
        mov     DWORD PTR [rdi+40], 2
        mov     QWORD PTR [rsi+160], 200
        ret

有没有某种方法可以告诉接受 gcc 扩展 asm 语法的编译器,asm 的输入包括指针及其可以指向的任何内容?


这是正确的;要求一个指针作为内联汇编的输入not暗示所指向的内存也是输入或输出或两者。对于寄存器输入和寄存器输出,对于所有 gcc 知道的 asm,只需通过屏蔽低位来对齐指针,或者向其添加一个常量。 (在这种情况下你会want它可以优化死店。)

The simple option is asm volatile and a "memory" clobber1.

您要求的更窄更具体的方式是使用“虚拟”内存操作数寄存器中的指针。您的 asm 模板没有引用此操作数(除非在 asm 注释中查看编译器选择的内容)。它告诉编译器你使用的是哪个内存actually读、写或读+写。

虚拟内存输入:"m" (*(const int (*)[]) iptr)
或输出:"=m" (*(int (*)[]) iptr)。或者当然"+m"使用相同的语法。

该语法正在转换为数组指针并取消引用,因此实际输入是 Carray。 (如果您实际上有一个数组,而不是指针,则不需要任何转换,只需将其作为内存操作数即可。)

如果您未指定尺寸[],它告诉 GCC 相对于该指针访问的任何内存都是输入、输出或输入/输出操作数。如果你使用[10] or [some_variable],告诉编译器具体的大小。对于运行时变量大小,gcc 实际上会错过以下优化:iptr[size+1] is not输入的一部分。

GCC 记录了这一点 https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Clobbers-and-Scratch-Registers-1因此支持它。我认为如果数组元素类型与指针相同,或者如果它是,则这不是严格别名违规char.

(来自海湾合作委员会手册)
一个 x86 示例,其中字符串内存参数的长度未知。

   asm("repne scasb"
    : "=c" (count), "+D" (p)
    : "m" (*(const char (*)[]) p), "0" (-1), "a" (0));

如果可以避免在指针输入操作数上使用早期破坏,则虚拟内存输入操作数通常会使用同一寄存器选择简单的寻址模式。

但是,如果您确实使用 Early-clobber 来保证 asm 循环的严格正确性,有时虚拟操作数会使 gcc 在内存操作数的基地址上浪费指令(以及额外的寄存器)。检查汇编output编译器的。


背景:

这是内联 asm 示例中的一个普遍错误,通常未被检测到,因为 asm 包装在一个函数中,该函数不会内联到任何调用者中,从而诱使编译器重新排序存储以进行合并,从而消除死存储。

GNU C 内联 asm 语法是围绕描述single给编译器的指令。目的是告诉编译器有关内存输入或内存输出的信息"m" or "=m"操作数约束,它选择寻址模式。

在内联汇编中编写整个循环需要小心确保编译器真正知道发生了什么(或者asm volatile plus a "memory"clobber),否则在更改周围代码或启用允许跨文件内联的链接时优化时,您将面临损坏的风险。

也可以看看使用内联汇编循环数组 https://stackoverflow.com/questions/34244185/looping-over-arrays-with-inline-assembly用于使用asm语句作为循环body,仍然在 C 中执行循环逻辑。使用实际(非虚拟)"m" and "=m"操作数,编译器可以通过使用它选择的寻址模式中的位移来展开循环。


脚注 1:A"memory"clobber 让编译器将 asm 视为非内联函数调用(可以读取或写入除本地内存之外的任何内存)逃逸分析 https://en.wikipedia.org/wiki/Escape_analysis已证明没有逃脱)。转义分析包括 asm 语句本身的输入操作数,还包括任何早期调用可能存储指针的任何全局或静态变量。因此,通常本地循环计数器不必在循环周围溢出/重新加载asm声明与"memory"破坏。

asm volatile有必要确保 asm 不会被优化,即使其输出操作数未使用(因为您需要发生未声明的写入内存的副作用)。

或者对于仅由asm读取的内存,如果相同的输入缓冲区包含不同的输入数据,则需要asm再次运行。没有volatile,asm语句可以是CSEd https://en.wikipedia.org/wiki/Common_subexpression_elimination脱离循环。 (A"memory"破坏者确实not使优化器在考虑是否将所有内存视为输入asm语句甚至需要运行。)

asm没有输出操作数是隐式的volatile,但最好将其明确化。 (GCC 手册有一节介绍asm 易失性 https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Volatile).

e.g. asm("... sum an array ..." : "=r"(sum) : "r"(pointer), "r"(end_pointer) : "memory")有一个输出操作数,因此不是隐式易失性的。如果你像这样使用它

 arr[5] = 1;
 total += asm_sum(arr, len);
 memcpy(arr, foo, len);
 total += asm_sum(arr, len);

Without volatile第二个asm_sum可以进行优化,假设具有相同输入操作数(指针和长度)的相同 asm 将产生相同的输出。你需要volatile对于任何不是其显式输入操作数的纯函数的汇编语言。如果不优化的话then the "memory"clobber 将达到要求内存同步的预期效果。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何指示可以使用内联 ASM 参数*指向*的内存? 的相关文章

随机推荐