优化掉 stores/construction 的可变堆栈变量是否合法?
Is it legal to optimize away stores/construction of volatile stack variables?
我注意到在某些情况下,clang 和 gcc 优化了在堆栈上声明的 volatile struct
的构造或赋值。例如下面的代码:
struct nonvol2 {
uint32_t a, b;
};
void volatile_struct2()
{
volatile nonvol2 temp = {1, 2};
}
Compiles on clang to:
volatile_struct2(): # @volatile_struct2()
ret
另一方面,gcc 不会删除存储,尽管它确实将两个隐含存储优化为一个存储:
volatile_struct2():
movabs rax, 8589934593
mov QWORD PTR [rsp-8], rax
ret
奇怪的是,clang 不会将易失性存储优化为单个 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
。
虽然似乎完全 "reasonable" 删除这些几乎不可能通过任何合理过程观察到的商店,但我的印象是,在标准 volatile
中,负载和商店被假定为一部分程序的可观察行为(除了调用 IO 函数),句号。这意味着它们不会被 "as if" 删除,因为根据定义它会改变程序的 可观察行为 。
是我错了,还是 clang 违反了这里的规则?也许 构造 被排除在必须假定 volatile
有副作用的情况之外?
让我们研究一下标准直接说的是什么。 volatile
的行为由一对语句定义。 [intro.execution]/7:
The least requirements on a conforming implementation are:
- Accesses through volatile glvalues are evaluated strictly according to the rules of the abstract machine.
...
和[intro.execution]/14:
Reading an object designated by a volatile glvalue (6.10), modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment.
嗯,[intro.execution]/14 不适用,因为上面的代码中没有任何内容构成 "reading an object"。你初始化它并销毁它;它从未被阅读。
所以剩下 [intro.execution]/7。这里重要的短语是"accesses through volatile glvalues"。虽然 temp
肯定是一个 volatile
值,而且它肯定是一个 glvalue...你实际上永远不会通过它访问。哦,是的,您初始化了对象,但实际上并没有将 "though" temp
作为泛左值访问。
也就是说,temp
作为表达式是一个泛左值,根据泛左值的定义:"an expression whose evaluation determines the identity of an object, bit-field, or function." 创建和初始化 temp
results[=64 的语句=] 在泛左值中,但是 temp
的初始化不是通过泛左值访问的。
将 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 的作者编写。也就是说,他们并没有充分利用措辞,因为这只是一些偶然代码组合在一起的奇怪怪癖。
Although gcc doesn't remove the construction in this particular case, it does perhaps even more aggressive optimizations in the case that the struct
members themselves are declared volatile
, rather than the struct
itself at the point of construction:
GCC 在这里的行为是:
- 不符合标准,因为违反了[intro.execution]/7,但是
- 绝对没有办法证明它不符合标准。
鉴于您编写的代码,用户根本无法检测这些读取和写入是否实际发生。而且我相当怀疑,当您做任何事情让外界看到它时,这些更改会突然出现在编译代码中。无论标准如何称呼它 "observable behavior",事实是根据 C++ 自己的内存模型,没有人可以看到它。
GCC 由于缺乏证人而逍遥法外。或者至少有可靠的证人(任何能看到它的人都会犯调用 UB)。
所以你不应该把 volatile
当作一些优化关闭开关。
从标准的角度来看,不要求实现记录任何对象在内存中的物理存储方式。即使实现记录了使用 unsigned char*
类型的指针访问特定类型对象的行为,也允许实现以其他方式物理存储数据,然后调整基于字符的读写代码适当的行为。
如果执行平台指定抽象机器对象与 CPU 所见存储之间的关系,并定义访问某些 CPU 地址可能触发副作用的方式,编译器不会'不知道,适合该平台上低级编程的高质量编译器应该生成代码,其中 volatile
限定对象的行为与该规范一致。该标准并未尝试强制所有实现都适用于低级编程(或任何其他特定目的)。
如果自动变量的地址从不暴露给外部代码,volatile
限定符只需要有两个作用:
如果在函数内调用 setjmp
,编译器必须做任何必要的事情来确保 longjmp
不会破坏任何 volatile
-合格的对象,即使它们写在 setjmp
和 longjmp
之间。如果没有限定符,在 setjmp
和 longjmp
之间写入的对象的值在执行 longjmp
时将变得不确定。
允许编译器假定任何没有副作用的循环将运行完成的规则不适用于在循环内访问易失性对象的情况,实现是否会定义可观察此类访问的任何方式。
除非在那些情况下,as-if 规则将允许编译器以与物理机无关的方式在抽象机中实现 volatile
限定符。
我注意到在某些情况下,clang 和 gcc 优化了在堆栈上声明的 volatile struct
的构造或赋值。例如下面的代码:
struct nonvol2 {
uint32_t a, b;
};
void volatile_struct2()
{
volatile nonvol2 temp = {1, 2};
}
Compiles on clang to:
volatile_struct2(): # @volatile_struct2()
ret
另一方面,gcc 不会删除存储,尽管它确实将两个隐含存储优化为一个存储:
volatile_struct2():
movabs rax, 8589934593
mov QWORD PTR [rsp-8], rax
ret
奇怪的是,clang 不会将易失性存储优化为单个 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
。
虽然似乎完全 "reasonable" 删除这些几乎不可能通过任何合理过程观察到的商店,但我的印象是,在标准 volatile
中,负载和商店被假定为一部分程序的可观察行为(除了调用 IO 函数),句号。这意味着它们不会被 "as if" 删除,因为根据定义它会改变程序的 可观察行为 。
是我错了,还是 clang 违反了这里的规则?也许 构造 被排除在必须假定 volatile
有副作用的情况之外?
让我们研究一下标准直接说的是什么。 volatile
的行为由一对语句定义。 [intro.execution]/7:
The least requirements on a conforming implementation are:
- Accesses through volatile glvalues are evaluated strictly according to the rules of the abstract machine.
...
和[intro.execution]/14:
Reading an object designated by a volatile glvalue (6.10), modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment.
嗯,[intro.execution]/14 不适用,因为上面的代码中没有任何内容构成 "reading an object"。你初始化它并销毁它;它从未被阅读。
所以剩下 [intro.execution]/7。这里重要的短语是"accesses through volatile glvalues"。虽然 temp
肯定是一个 volatile
值,而且它肯定是一个 glvalue...你实际上永远不会通过它访问。哦,是的,您初始化了对象,但实际上并没有将 "though" temp
作为泛左值访问。
也就是说,temp
作为表达式是一个泛左值,根据泛左值的定义:"an expression whose evaluation determines the identity of an object, bit-field, or function." 创建和初始化 temp
results[=64 的语句=] 在泛左值中,但是 temp
的初始化不是通过泛左值访问的。
将 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 的作者编写。也就是说,他们并没有充分利用措辞,因为这只是一些偶然代码组合在一起的奇怪怪癖。
Although gcc doesn't remove the construction in this particular case, it does perhaps even more aggressive optimizations in the case that the
struct
members themselves are declaredvolatile
, rather than thestruct
itself at the point of construction:
GCC 在这里的行为是:
- 不符合标准,因为违反了[intro.execution]/7,但是
- 绝对没有办法证明它不符合标准。
鉴于您编写的代码,用户根本无法检测这些读取和写入是否实际发生。而且我相当怀疑,当您做任何事情让外界看到它时,这些更改会突然出现在编译代码中。无论标准如何称呼它 "observable behavior",事实是根据 C++ 自己的内存模型,没有人可以看到它。
GCC 由于缺乏证人而逍遥法外。或者至少有可靠的证人(任何能看到它的人都会犯调用 UB)。
所以你不应该把 volatile
当作一些优化关闭开关。
从标准的角度来看,不要求实现记录任何对象在内存中的物理存储方式。即使实现记录了使用 unsigned char*
类型的指针访问特定类型对象的行为,也允许实现以其他方式物理存储数据,然后调整基于字符的读写代码适当的行为。
如果执行平台指定抽象机器对象与 CPU 所见存储之间的关系,并定义访问某些 CPU 地址可能触发副作用的方式,编译器不会'不知道,适合该平台上低级编程的高质量编译器应该生成代码,其中 volatile
限定对象的行为与该规范一致。该标准并未尝试强制所有实现都适用于低级编程(或任何其他特定目的)。
如果自动变量的地址从不暴露给外部代码,volatile
限定符只需要有两个作用:
如果在函数内调用
setjmp
,编译器必须做任何必要的事情来确保longjmp
不会破坏任何volatile
-合格的对象,即使它们写在setjmp
和longjmp
之间。如果没有限定符,在setjmp
和longjmp
之间写入的对象的值在执行longjmp
时将变得不确定。允许编译器假定任何没有副作用的循环将运行完成的规则不适用于在循环内访问易失性对象的情况,实现是否会定义可观察此类访问的任何方式。
除非在那些情况下,as-if 规则将允许编译器以与物理机无关的方式在抽象机中实现 volatile
限定符。