避免在内存访问时刷新寄存器(gcc inline asm)

Avoid flushing registers on memory access (gcc inline asm)

在 GCC 手册的 6.43.2.5 Extended Asm - Clobbers 节中,在对 "memory" clobber 的解释中,提到了一个避免刷新寄存器的技巧:

If you know the size of the memory being accessed at compile time, you may specify a memory input like this:

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

(Assuming you access a string of 10 bytes.)

我想我理解这个想法,但我并不完全清楚如何使用它以及这个技巧有什么含义——除了向 GCC 提供更多类型信息之外。 最重要的三个问题出现了:

既然我们喜欢示例:这段代码是否有意义并且合法?好像还行。

#include <iostream>
#include <cstdint>

void add_assembly(std::uint64_t * x) {
  struct memory { std::uint64_t data[2]; } * p = reinterpret_cast<memory*>(x);
  __asm__ (
        "addq , %[x] \t\n"
        "addq , 8%[x] \t\n"
        : [x] "+m" (*p) // Bonus question: Why don't I need a "&" here?
        : "m" (*p)
        : "cc"
  );
}

int main() {
  std::uint64_t x[2];
  x[0] = 3000;
  x[1] = 7253;
  std::cout << "before: " << x[0] << " " << x[1] << std::endl;
  add_assembly(&x[0]); // add 1 to x[0], add 5 to x[1]
  std::cout << "after:  " << x[0] << " " << x[1] << std::endl;
  return 0;
}

我不是 100% 确定这个答案中的所有内容,但我最近确实看过这个问题。

May I also use this as an output or input/output operand and modify the data?

是的,但是使用单独的 r(寄存器)或 m(可能是复杂的寻址模式)约束可能会更好。这是特别的。如果您想在循环中递增指针,因此需要在寄存器中,则为 true。 m 约束可以使 %0 扩展为 (%rsi, %rdx, 4) 或其他东西。

If I use this trick, do I still need the "memory" clobber?

没有。这告诉 gcc 哪些内存可能被修改。 "memory" 表示 所有 内存可能被修改。

Can I safely drop the volatile qualifier needed when only accessing memory?

你是说asm volatile (...)?是的。 volatile 只需要让 gcc 在周围代码不使用其输出(或没有任何输出)时不移动或消除 asm 语句。只要你告诉 gcc 你的 asm 在内存中产生的结果,并使用那个结果,你就不应该使用 volatile,所以 gcc 可以像它可以围绕任何其他黑盒函数一样进行优化。 (当然假设你的 asm 是一个 "pure" 函数,它只依赖于它声明的输入。)


例子很好。 :) 你的看起来不错。

我认为您是对的,指定读写输入操作数是不够的;您需要将其指定两次,作为输入和输出。 + 约束的写法让我认为 "read and written" 意味着 gcc 将你留在其中的值视为变量的新值,但我认为并非如此。

我认为在 x86 上,m 约束等同于 o(可偏移)约束。但要注意语法;您不希望 8%[x] 变成 88(%rsp)。那么也许 8 + %[x]?不确定。在离开 space,所以这是一个语法错误,而不是一个不同的数字。

\t\n 很傻。您需要每行开头的制表符。

asm (
    "inc      %[x]   \n\t"   // inc is shorter than `add`
    "addq , 8 %[x] \n\t"   // this `add` writes all flags, preventing partial-flag stalls / slowdowns
    : [x] "+m" (*p) // Bonus question: Why don't I need a "&" here?
         // because you aren't writing a temporary to the output before reading the input
    : "m" (*p)
    : "cc"
);