gcc 11.1 中 volatile 的非一致性优化

Posted

技术标签:

【中文标题】gcc 11.1 中 volatile 的非一致性优化【英文标题】:Non-conforming optimizations of volatile in gcc 11.1 【发布时间】:2022-01-19 15:10:09 【问题描述】:

在撰写有关编译器必须如何处理 volatile 的答案时,我相信我可能偶然发现了一个 gcc 错误,并希望有人在我报告它之前进行验证。

我写了一个这样的简单函数:

int foo (int a, int b, int c)

  b = a + 1;
  c = b + 1;
  a = c + 1;
  return a;

如果不进行优化,这会导致大量无意义的数据来回移动。通过优化,编译器只需获取存储a 的寄存器,然后添加 3 并返回该结果。说 x86 lea eax, [rdi+3]ret。这是意料之中的,到目前为止一切顺利。

为了演示顺序和易失性访问,我将示例更改为:

int foo (int a, int b, int c)

  b = a + 1;
  c = *(volatile int*)&b + 1;
  a = c + 1;
  return a;

这里有一个对 b 内容的左值访问,它是 volatile 限定的,据我所知,绝对不允许编译器优化该访问1)。从 gcc 4.1.2(可能更早)到 gcc 10.3,我得到了一致的行为(在 clang 中也是如此)。即使使用-O3,x86 机器代码也看起来像这样:

foo:
        add     edi, 1
        mov     DWORD PTR [rsp-4], edi
        mov     eax, DWORD PTR [rsp-4]
        add     eax, 2
        ret

然后我在 gcc 11.1 及更高版本上尝试相同,现在我得到:

foo:
        lea     eax, [rdi+3]
        ret

https://godbolt.org/z/e5x74z3Kb

ARM gcc 11.1 做了类似的事情。

这是编译器错误吗?


1) 参考:ISO/IEC 9899:2018 5.1.2.3,特别是 §2、§4 和 §6。

【问题讨论】:

我认为指针被认为是易失性的,它的值不会被优化,但它指向的内存不是。 @sorush-r 这并不重要。我告诉编译器“你必须从这里的内存中读取这个变量”,但事实并非如此。假设我有一些原因,例如虚拟读取堆上的变量以确保在我第一次使用它时现在而不是稍后执行堆分配。易失性访问副作用可以通过多种方式影响程序。 @sorush-r:不,它是指向volatile int 的指针。您所描述的将是*(int *volatile)&b,并且确实可以优化访问,即使使用没有此问题中描述的错误(?)的旧 GCC(如 9.4)。 godbolt.org/z/bs31xveYK(转换的 volatile 限定的指针对象结果永远不会在任何地方实现,这很好,因为它只是一个右值) @sorush-r volatile int* 是一个指向易失数据的指针。 看起来像编译器错误,类似于this。在这两种情况下,编译器都可以随意假设自动变量不能是“易失的”(这是完全正确的,除了调试程序的情况,其中变量可以在运行时更改)。 【参考方案1】:

我听到一个编译器团队令人信服地争论(好吧,我差点睡着了,所以我得到了一个粗略的大纲),在外部作用域的字大小对象之外,易失性是一种毫无意义的装饰。此外,编译器提供了一些围绕无意义属性对象的传统行为,以方便使用遗留代码的人。这种解释是基于对 C 标准的荒谬简化,这比正确要好,它是 技术上正确的,是 alpha-geeks 的黄金标准。

【讨论】:

那你怎么解释这个:godbolt.org/z/bKfTqdGzr。我创建了一个局部变量 volatile,现在甚至 gcc 11 也尝试尊重该限定符,同时仍在执行优化 add edi, 2 我没有说任何规定;我只是间接地重复了前 10 名编译器提供商之一的开发人员的意见。标准解释只能通过处方进行;您当然应该等待几个小时才能操作重型机械。 volatile 是一种毫无意义的装饰 鉴于volatile 将所有通过编写的代码访问的变量变成可观察到的副作用(根据定义),一个编译器团队所有将其描述为“毫无意义的装饰”的团体都有点吓人。我不知道如何挥手消除所需的副作用可能是“技术上正确的”。 在 OPs 示例中,函数参数是不可观察的。由于它是不可观察的,因此没有合同。自动定义只有在其地址已被记录时才能被观察到。注意y = *&x不记录x的地址;但 *&y = x` 可能。标准的好处是有很多对它们的解释。【参考方案2】:

根据 C18 5.1.2.3/6,对 volatile 对象的访问(严格按照抽象机的规则)是程序可观察行为的一部分,所有符合要求的实现都必须重现。在此上下文中,术语“访问”包括读取和写入。

C18 5.1.2.3/2 和 /4 强调 volatile 访问是需要的副作用,排除在允许实现避免产生不必要的副作用的规则之外。

我看到的 GCC 的唯一结果是一个论点,即尽管 (volatile int*)&b 是具有 volatile 限定类型的左值,但它可以证明它指定的对象 (b) 实际上不是“易失性对象” ",如果你按照它的声明去的话,确实不是这样。这与 GCC 11.2 对此版本的函数观察到的行为一致:

int foo (int a, int b, int c)

  volatile int bv = a + 1;
  c = bv + 1;
  a = c + 1;
  return a;

,它产生的程序集与旧版本的 GCC 对原始代码 (godbolt) 的程序集相同。

这是否构成不符合语言标准的错误尚不清楚,但 GCC 肯定阻碍了程序员的明显意图。

【讨论】:

【参考方案3】:

将地址传递给非内联函数使 GCC 尊重 volatile 转换,以便以后访问(可能更早,未检查)到函数 arg 或本地。 https://godbolt.org/z/cssveev7n

我复制了 c = 行,并且由于使用 GCC 主干的 volatile cast,asm 包含两个 b 负载。

void bar(void*);
int foo (int a, int b, int c)

  bar(&b);              // b's address has now "escaped" - potentially globally visible
  b = a + 1;

  c = *(volatile int*)&b + 1;
  c = *(volatile int*)&b + 1;   // both accesses present.
  a = c + 1;
  return a;

# GCC trunk -O3 -fverbose-asm
        call    bar     #
        mov     DWORD PTR [rsp+12], ebx   # b, tmp89
        mov     eax, DWORD PTR [rsp+12]   # _2, MEM[(volatile int *)&b]
        mov     eax, DWORD PTR [rsp+12]   # _3, MEM[(volatile int *)&b]
 ... 
        add     eax, 2
        ret

所以这似乎是无辜的,除非在一些微基准用例中;它不会使用像 the Linux kernel's READ_ONCE / WRITE_ONCE macros 这样的强制转换来破坏手工滚动的原子。

仍然可以说违反了 ISO C 规则,如果将普通的 int 别名为 volatile int 是合法的。如果不是,它只是 GCC 定义的行为,所以它取决于 GCC。我将其更多地作为数据点发布,而不是在问题的那个方面的任一方向的论点。

【讨论】:

bar 调用将阻止优化,它只是抓取(包含寄存器的)a,添加 3 并返回结果,因此它必须尊重 volatile 访问,因为它可以不再推理b 的内容。不过,在您的示例中,您确实得到了额外的预期 mov 指令,如果修改为 c = *(int*)&b + 1;,则不会发生这种情况。 @Lundin:是的,no-cast 代码也更复杂,但我的第一个想法是“这个 bug 会破坏手动滚动的原子”,事实证明它不会。在任何(或至少在这种情况下)该值可能对其他线程可见的情况下,volatile 都会受到尊重。我很好奇这是否是故意的 GCC 更改以忽略 volatile 对已知私有自动存储的访问。 @Lundin:理论上,对于使用 volatile cast(而不是像 volatile int c 这样的 volatile var)或内联 asm 来强制编译器实现值的微基准测试是有问题的被分配。它似乎适用于写入和读取:godbolt.org/z/sK5bofWeq 显示 *(volatile int*)&c = ... 两次没有停止对寄存器的完全优化,函数调用被注释掉。但是,如果 volatile int c 不使用内联 asm,那么使用 volatile int c 就可以了。 在嵌入式系统中,您经常执行这样的易失性读取来强制执行“虚拟访问”,尽管在这种情况下,目标变量几乎总是一个 volatile 限定变量。

以上是关于gcc 11.1 中 volatile 的非一致性优化的主要内容,如果未能解决你的问题,请参考以下文章

GCC 11.1 进行 C++ 多项优化

11.1 线程简介

极客日报第117期:鸿蒙OS最快6月正式推送;微信Windows版将支持刷朋友圈;GCC 11.1正式发布

GCC中可执行文件的非GOT样式重定位

volatile

volatile关键字