我可以告诉编译器我需要提前破坏内存操作数吗?

Can I tell the compiler that I need to earlyclobber a memory operand?

考虑这个程序,它可以编译为 32 位或 64 位:

#include <stdio.h>

static int f(int x, int y) {
    __asm__(
        "shrl , %0\n\t"
        "movl %1, %%edx\n\t"
        "addl %%edx, %0"
        : "+r"(x)      // needs "+&r" to work as intended
        : "r"(y)
        : "edx"
    );
    return x;
}

int main(void) {
    printf("0x%08X\n", f(0x10000000, 0x10000000));
}

-O1 或更高时,它给出了错误的答案(0x02000000 而不是 0x11000000),因为 xy 被读取之前被写入,但是 x 的约束没有 & 来指定 earlyclobber,因此编译器将它们放在同一个寄存器中。如果我将 +r 更改为 +&r,那么它会按预期再次给出正确答案。

现在考虑这个程序:

#include <stdio.h>

static int f(int x, int y) {
    __asm__(
        "shrl , %0\n\t"
        "movl %1, %%edx\n\t"
        "addl %%edx, %0"
        : "+m"(x)        // Is this safe without "+&m"?  Compilers reject that
        : "m"(y)
        : "edx"
    );
    return x;
}

int main(void) {
    printf("0x%08X\n", f(0x10000000, 0x10000000));
}

除了使用m约束代替r约束外,其他完全一样。现在即使没有 &,它也恰好给出了正确答案。但是,我知道依赖它是一个坏主意,因为我在读取 y 之前仍在写入 x 而没有告诉编译器我正在这样做。但是当我将 +m 更改为 +&m 时,我的程序不再编译:GCC 告诉我 error: input operand constraint contains '&',而 Clang 告诉我 invalid output constraint '+&m' in asm。为什么这不起作用?

我想到了两种可能:

  1. 尽早破坏内存中的内容总是安全的,因此 & 被拒绝为冗余
  2. 过早破坏内存中的东西从来都不安全,因此 & 被拒绝为无法满足

是其中之一吗?如果是后者,最好的解决方法是什么?还是这里发生了其他事情?

我认为 "+m""=m" 没有明确的 & 是安全的。

来自the docs,我强调的是:

&
Means (in a particular alternative) that this operand is an earlyclobber operand, which is written before the instruction is finished using the input operands. Therefore, this operand may not lie in a register that is read by the instruction or as part of any memory address.

过度解释这可能是有问题的,但考虑到它在实践中似乎是安全的,并且有充分的理由说明为什么会这样,我认为文档的以下解释(即 GCC 的保证行为) 合理:

"内存地址"是指寻址方式本身,例如例如,如果您有一个 "m"(foo) 内存操作数,GCC 会发明并替换 %1 之类的 16(%rdx)。它不是在谈论早期破坏指向的内存,只是在寻址模式下可能被读取的寄存器。

这意味着 GCC 需要避免在任何寻址模式下选择与早期破坏寄存器操作数相同的寄存器。这使您可以在与 "=&r" 操作数相同的语句中安全地使用 "m" 操作数(和 +m 或 =m),就像您可以使用 "r" 操作数一样。需要用 & 标记的是寄存器输出操作数,而不是潜在的读者。

它在寄存器 中明确表示 的事实意味着这只是寄存器操作数的一个问题,而不是内存。


在C抽象机中,每个对象都有一个内存地址(register int foo除外)。

认为 编译器总是会为 "m" / "+m" 操作数选择该地址,而不是一些临时发明的地址。例如,我认为 lea 是安全的/支持内存操作数并将地址存储在某处,如果在 C 中 tmp = &foo; 是安全的。

你可以把"earlyclobber"想成"don't pick the same location as any input operand"。由于不同的对象有不同的地址,这已经免费为内存发生了。

当然,除非您为单独的输入和输出操作数指定相同的对象。在 "=&r"(foo)"r"(foo) 的寄存器情况下,您 为输入和结果获得单独的寄存器。但不是为了内存,即使你使用早期破坏的 "=&m"(foo) 操作数,它确实编译,即使 "+&m" 没有。

随机事实,experiments on Godbolt

  • "m"(y+1) 不能作为输入:"memory input 1 is not directly addressable"。但它适用于寄存器。内存源操作数可能必须是 C 抽象机中存在的对象。

  • "+&m"(x) 不编译:error: input operand constraint contains '&'

    "=&m"(x) 编译干净。但是,它的 "0"(x) 匹配约束会收到警告:warning: matching constraint does not allow a registerhttps://godbolt.org/z/4kKNq4

    + 操作数似乎在内部实现为单独的输出和输入操作数,具有匹配约束以确保它们选择相同的位置。(更多证据:如果您只使用一个"+r"操作数,你可以在asm模板中引用%1而不会出现警告,它和%0是同一个寄存器。)

    似乎 "=&m"(x)"m"(x) 总是选择相同的内存 anyway,即使没有匹配约束。 (出于同样的原因,它与任何其他对象的内存不同,这就是为什么 "+&m"(x) 是多余的。)


如果两个 C 对象的生命周期重叠,它们的地址将是不同的。 所以我认为这就像将指向局部变量的指针传递给非内联函数一样,到目前为止作为优化器而言。它无法在它们之间发明别名。例如

  int x = 1;
  {
    int tmp = x;     // dead after this call.
    foo(&x, &tmp);
  }

例如,上面的代码不能为 foo 的两个操作数传递相同的地址(例如通过优化掉 tmp)。对于带有 "=m(x)""m"(tmp) 操作数的 inline-asm 语句也是如此。无需早期破坏。

很多推理都是根据人们合理预期它的工作方式推断出来的,但这与它在实践中的工作方式以及文档中的措辞是一致的。 我提到这一点是为了警告不要在没有文档支持的情况下对其他情况应用相同的推理。


回复:第 2 点:即使需要早期破坏,它也总是可以满足内存需求。每个对象都有自己的地址。如果您将重叠的联合成员作为内存输入和输出传递,那是程序员的错。如果它不存在于源代码中,编译器将不会创建这种情况。例如如果这意味着内存输入与内存输出重叠,则它不会省略临时变量。 (或者根本没有)。