【问题标题】:Is it legal to optimize away stores/construction of volatile stack variables?优化 volatile 堆栈变量的存储/构造是否合法?
【发布时间】:2018-04-10 05:21:39
【问题描述】:

我注意到,在某些情况下,clang 和 gcc 优化了栈上声明的 volatile struct 的构造或赋值。例如以下代码:

struct nonvol2 {
    uint32_t a, b;
};

void volatile_struct2()
{
    volatile nonvol2 temp = {1, 2};
}

Compiles 铿锵到:

volatile_struct2(): # @volatile_struct2()
  ret

另一方面,gcc 不会删除存储,尽管它确实将两个隐含存储优化为一个:

volatile_struct2():
        movabs  rax, 8589934593
        mov     QWORD PTR [rsp-8], rax
        ret

奇怪的是,clang 不会将 volatile 存储优化为单个 int 变量:

void volatile_int() {
    volatile int x = 42;
}

编译为:

volatile_int(): # @volatile_int()
  mov dword ptr [rsp - 4], 1
  ret

此外,具有 1 个成员而不是 2 个成员的结构不会被优化掉。

虽然 gcc 在这种特殊情况下不会删除构造,但在 struct 成员本身被声明为 volatile 而不是 struct 本身的情况下,它可能会进行更积极的优化建设:

typedef struct {
    volatile uint32_t a, b;
} vol2;

void volatile_def2()
{
    vol2 temp = {1, 2};
    vol2 temp2 = {1, 2};
    temp.a = temp2.a;
    temp.a = temp2.a;
}

简单地编译成一个简单的ret

虽然删除这些几乎不可能通过任何合理过程观察到的存储似乎完全“合理”,但我的印象是,在标准 volatile 中,加载和存储被假定为 observable 的一部分程序的行为(除了对 IO 函数的调用),句号。这意味着它们不会被“好像”删除,因为根据定义它会改变程序的可观察行为

这是我错了,还是 clang 违反了这里的规则?也许 construction 被排除在必须假定volatile 具有副作用的情况之外?

【问题讨论】:

  • 这使用 Apple LLVM 版本 8.1.0 (clang-802.0.42) 在 macOS 10.12.6 上针对 x86_64 进行复制,使用“-O3”编译。我必须在源代码中插入#include <stdint.h>。使用二元素结构,写入被优化掉。对于单元素结构,它不是。
  • @EricPostpischil - 我在问题中包含了godbolt link,尽管它在那里可能不是很明显。您可以通过将参数更改为使用--version 来获取所需的信息。我得到了:clang version 5.0.0 (tags/RELEASE_500/final 312636), Target: x86_64-unknown-linux-gnu, Thread model: posix。据我所知,这种行为一直是相同的(回到 clang 3.0)。
  • 它也用 C 而不是 C++ 重现。所以这不是 C++ 构造函数问题。我认为这可能是一个真正的编译器错误。 (而且,即使我们对需要 volatile 访问是错误的 [因此编译器优化二元素结构访问是正确的],编译器不优化一元素结构访问也是错误的。[错误,因为它错过了一种优化,不是因为它违反了语言规范。])
  • 半相关:C11 中的struct-assignment 不是成员,因此它忽略了成员上的_Atomic 限定符(可能还有volatile)。 (在 C++11 中,atomic<T> 没有复制构造函数,并且它确实按成员进行,所以它不编译。)godbolt.org/g/heUd1R。我正在考虑就这个很少记录的事实提出一个问题,但我想我通过查看标准回答了我自己的 C11 问题,gcc 可能应该发出警告,但很明显,只使用 memcpy 或 rep stosq 是正确的行为(从零结构复制)。
  • C 或 C++ 中的结构赋值或初始化不能解释为什么 gcc 对成员访问的易失性成员进行优化。 (你的最后一个例子。为 gcc 和 clang godbolt.org/g/1oGFq4 添加到 Godbolt(C11 和 C++11,尽管事实证明移植到 C11 不会改变任何编译器的 asm 输出)。)

标签: c++ optimization x86 language-lawyer volatile


【解决方案1】:

从标准的角度来看,没有要求实现记录任何对象如何物理存储在内存中的任何内容。即使实现记录了使用 unsigned char* 类型的指针访问特定类型的对象的行为,也允许实现以其他方式物理存储数据,然后让基于字符的读写代码适当地调整行为.

如果一个执行平台指定了抽象机器对象和 CPU 看到的存储之间的关系,并定义了对某些 CPU 地址的访问可能会触发编译器不知道的副作用的方式,那么一个适合低质量的编译器该平台上的级别编程应该生成代码,其中volatile 限定对象的行为与该规范一致。该标准并未尝试强制要求所有实现都适用于低级编程(或任何其他特定目的,就此而言)。

如果自动变量的地址从不暴露给外部代码,volatile 限定符只需要有两个效果:

    1234563 setjmplongjmp。如果没有限定符,则在执行longjmp 时,写入在setjmplongjmp 之间的对象的值将变得不确定。
  1. 允许编译器假定任何没有副作用的循环将运行到完成的规则​​不适用于在循环内访问 volatile 对象的情况,无论实现是否定义可以观察到此类访问的任何方式。

除非在这些情况下,as-if 规则将允许编译器以与物理机器无关的方式在抽象机器中实现 volatile 限定符。

【讨论】:

    【解决方案2】:

    让我们调查一下标准直接说明了什么。 volatile 的行为由一对语句定义。 [intro.execution]/7:

    对一致性实现的最低要求是:

    • 通过 volatile glvalues 进行的访问严格按照抽象机的规则进行评估。

    ...

    还有 [intro.execution]/14:

    读取由 volatile glvalue (6.10) 指定的对象、修改对象、调用库 I/O 函数或调用执行任何这些操作的函数都是副作用,它们是执行环境。

    好吧,[intro.execution]/14 不适用,因为上述代码中没有任何内容构成“读取对象”。你初始化它并销毁它;它永远不会被阅读。

    所以剩下 [intro.execution]/7。这里重要的短语是“访问通过 volatile glvalues”。虽然temp 肯定是volatile 值,但它肯定是一个glvalue……你永远不会真正通过它访问。哦,是的,您初始化了对象,但实际上并没有将“虽然”temp 作为泛左值访问。

    也就是说,temp 作为表达式是一个 glvalue,根据 glvalue 的定义:“一个表达式,其评估确定对象、位域或函数的身份。”创建和初始化temp 的语句结果 在一个glvalue 中,但temp 的初始化不是通过glvalue 访问的。

    volatile 视为const。关于const 对象的规则在它被初始化之后 才适用。同样,volatile 对象的规则在初始化之后才适用。

    所以volatile nonvol2 temp = {1, 2};volatile nonvol2 temp; temp.a = 1; temp.b = 2; 之间是有区别的。当然还有 Clang does the right thing in that case

    话虽如此,Clang 在这种行为方面的不一致(仅在使用结构时优化它,并且仅在使用包含多个成员的结构时)表明这可能不是由Clang的作者。也就是说,他们并没有充分利用措辞,这只是一些意外代码组合在一起的奇怪怪癖。


    尽管 gcc 在这种特殊情况下不会删除构造,但在 struct 成员本身被声明为 volatile 而不是 struct 本身的情况下,它可能会进行更积极的优化建设:

    GCC 的行为是:

    1. 不符合标准,因为它违反了 [intro.execution]/7,但是
    2. 绝对没有办法证明它不符合标准。

    鉴于您编写的代码,用户根本无法检测这些读取和写入是否实际发生。而且我宁愿怀疑,当你做任何事情让外界看到它时,那些变化会突然出现在编译的代码中。不管标准多么希望将其称为“可观察的行为”,事实是通过 C++ 自己的内存模型,没有人可以看到它

    由于缺乏证人,GCC 侥幸逃脱。或者至少是可信的证人(任何能看到它的人都会犯援引 UB)。

    因此,您不应将volatile 视为某种优化关闭开关。

    【讨论】:

    猜你喜欢
    • 2015-04-18
    • 1970-01-01
    • 2017-04-17
    • 2018-12-30
    • 1970-01-01
    • 2017-11-05
    • 2014-02-17
    • 2015-03-09
    相关资源
    最近更新 更多