【问题标题】:Why is the compiler dedicating a memory location for storing a redundant variable in this case?在这种情况下,为什么编译器会专门分配一个内存位置来存储冗余变量?
【发布时间】:2021-09-17 03:22:26
【问题描述】:

wrote this简单的C++代码,看看原子变量是怎么实现的。

#include <atomic>

using namespace std;

atomic<float> f(0);

int main() {
    f += 1.0;
}

它正在为 -O3 中的 main 生成此程序集:

main:
        mov     eax, DWORD PTR f[rip]
        movss   xmm1, DWORD PTR .LC1[rip]
        movd    xmm0, eax
        mov     DWORD PTR [rsp-4], eax  ; this line is redundant
        addss   xmm0, xmm1
.L2:
        mov     eax, DWORD PTR [rsp-4]  ; this line is redundant
        movd    edx, xmm0
        lock cmpxchg    DWORD PTR f[rip], edx
        je      .L5
        mov     DWORD PTR [rsp-4], eax  ; this line is redundant
        movss   xmm0, DWORD PTR [rsp-4]  ; this line can become  movd    xmm0, eax
        addss   xmm0, xmm1
        jmp     .L2
.L5:
        xor     eax, eax
        ret
f:
        .zero   4
.LC1:
        .long   1065353216

它是使用原子比较和交换技术来实现原子性。但是在那里,旧值存储在 [rsp-4] 的堆栈中。但是在上面的代码中,eax 是不变的。因此,旧值保留在 eax 本身中。为什么编译器为旧值分配额外的空间?即使在-O3!是否有任何特定原因将该变量存储在堆栈中而不是寄存器中?

编辑:逻辑推论-

有 4 行使用rsp-4 -

mov     DWORD PTR [rsp-4], eax    --- 1
mov     eax, DWORD PTR [rsp-4]    --- 2  <--.
mov     DWORD PTR [rsp-4], eax    --- 3     | loop
movss   xmm0, DWORD PTR [rsp-4]   --- 4  ---'

第 3 行和第 4 行之间绝对没有其他内容,因此可以使用 3 将第 4 行重写为
movd xmm0, eax

现在,当在循环中从第 3 行转到第 2 行时,rsp-4(也没有eax)没有任何修改。所以这意味着第 3 行和第 2 行依次折叠到
mov eax, eax
这本质上是多余的。

最后,只剩下第 1 行了,它的目的地不再被使用。所以也是多余的。

【问题讨论】:

  • @Yksisarvinen 是的,这很简单。看到之后,我搬到了花车上;)
  • @SouravKannanthaB 他们回应了我的一个有点误导性的评论,想知道这是否与浮动没有原子加法有关。
  • clang 的代码更合理:godbolt.org/z/qeGK8KzW4。这看起来像是一个简单的错过优化错误,您可以报告它。

标签: c++ assembly gcc x86-64 atomic


【解决方案1】:

是否有任何特定原因将该变量存储在堆栈中而不是寄存器中?

归根结底,线程间通信存在原子,您不能跨线程共享寄存器。

您可能认为 gcc 可以检测从未与其他任何东西共享的局部变量原子并将它们降级为常规变量。然而:

  1. 我个人不明白这会带来什么,因为在这些情况下您不应该使用原子。
  2. 无论如何,该标准似乎都禁止这种优化:

intro.races-14

由评估 B 确定的原子对象 M 的值应是由修改 M 的某些副作用 A 存储的值,其中 B 不会在 A 之前发生。

这里的关键词是副作用,这意味着对实际内存存储的修改是没有争议的。它必须发生。

就修改后的问题而言:

但在上面的代码中,eax是不变的

不幸的是,它不是。 cmpxchgeax 进行读取和写入操作,因此需要在循环的每次迭代中重新分配它。

需要循环,因为为了在原子浮点上执行+= 1。编译器必须继续尝试,直到它能够以足够快的速度执行读-增量-写序列,以至于原子在此期间不会发生变化。

【讨论】:

  • you can't share a register across threads 中间结果不需要共享给其他线程。无论如何堆栈不与其他线程共享..(我已经编辑了我的问题以准确标记我看到多余的行。请检查它。)
  • @SouravKannanthaB 将指向堆栈分配对象的指针共享给其他线程是完全可行的。就中间结果而言,我会看看是否可以更新答案以反映修改后的问题。
  • @SouravKannanthaB 我已经修改了答案
  • 还是[rsp - 4]是多余的。。虽然eax是循环修改的,但实际上是需要的修改。。去掉上面提到的多余的行,分析一下。您将获得完全相同的数据流。
  • @SouravKannanthaB:没错; GCC 在float 数据上使用整数cmpxchg 的“配方”显然会误导它进行冗余存储,而不仅仅是movd。使用-march=haswell 或其他东西而不是默认的通用调整是否有帮助?另请参阅Atomic double floating point or SSE/AVX vector load/store on x86_64 以获得纯加载和纯存储的一些代码生成(更愚蠢,因为它坚持使用整数加载/存储而不是原子 SSE2 纯加载。)我的回答还有关于无用存储/重新加载的 cmets在 RMW 中。
猜你喜欢
  • 2014-05-31
  • 2020-04-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-03
  • 1970-01-01
相关资源
最近更新 更多