[英]Why doesn't gcc remove this check of a non-volatile variable?
这个问题主要是学术问题。 我出于好奇而不是因为这给我带来了实际问题。
考虑以下不正确的C程序。
#include <signal.h>
#include <stdio.h>
static int running = 1;
void handler(int u) {
running = 0;
}
int main() {
signal(SIGTERM, handler);
while (running)
;
printf("Bye!\n");
return 0;
}
此程序不正确,因为处理程序会中断程序流,因此可以随时修改running
,因此应将其声明为volatile
。 但是,让我们说程序员忘记了这一点。
gcc 4.3.3,带有-O3
标志,将循环体(在running
标志的一次初始检查之后)编译为无限循环
.L7:
jmp .L7
这是可以预料的。
现在我们在while
循环中添加一些简单的东西,例如:
while (running)
putchar('.');
突然间,gcc不再优化循环条件了! 循环体的组件现在看起来像这样(再次在-O3
):
.L7:
movq stdout(%rip), %rsi
movl $46, %edi
call _IO_putc
movl running(%rip), %eax
testl %eax, %eax
jne .L7
我们看到每次循环都会从内存中重新加载running
; 它甚至没有缓存在寄存器中。 显然gcc现在认为running
的价值可能已经改变了。
那么为什么gcc突然决定在这种情况下需要重新检查running
的价值呢?
在一般情况下,编译器很难确切地知道函数可能访问哪些对象,因此可能会修改。 在调用putchar()
,GCC不知道是否存在可能能够修改running
的putchar()
实现,因此它必须有些悲观,并假设running
实际上可能已经更改。
例如,稍后在翻译单元中可能会有putchar()
实现:
int putchar( int c)
{
running = c;
return c;
}
即使在转换单元中没有putchar()
实现,也可能会有一些东西,例如,传递running
对象的地址,以便putchar
可以修改它:
void foo(void)
{
set_putchar_status_location( &running);
}
请注意,您的handler()
函数是全局可访问的,因此putchar()
可能会调用handler()
本身(直接或其他方式),这是上述情况的一个实例。
另一方面,由于
running
仅对转换单元可见(是
static
),所以当编译器到达文件末尾时,它应该能够确定
putchar()
没有机会访问它(假设是这种情况),编译器可以返回并“修复”while循环中的悲观化。
由于running
是静态的,编译器可能能够确定无法从翻译单元外部访问它并进行您正在讨论的优化。 但是,由于可以通过handler()
访问它,而handler()
可以从外部访问,因此编译器无法优化访问。 即使你使handler()
静态,它也可以从外部访问,因为你将它的地址传递给另一个函数。
请注意,在您的第一个示例中,即使我在上一段中提到的仍然是正确的,编译器也可以优化对running
的访问,因为C语言基于的“抽象机器模型”不考虑异步活动,除非在非常有限的情况下(其中一个是volatile
关键字,另一个是信号处理,尽管信号处理的要求不足以阻止编译器在第一个示例中优化掉running
的访问权限)。
事实上,在这些确切的情况下,这是C99关于抽象机器行为的内容:
5.1.2.3/8“程序执行”
例1:
实现可以定义抽象语义和实际语义之间的一对一对应关系:在每个序列点,实际对象的值将与抽象语义指定的值一致。 关键字
volatile
将是多余的。或者,实现可以在每个转换单元内执行各种优化,使得实际语义仅在跨转换单元边界进行函数调用时才符合抽象语义。 在这样的实现中,在每个函数入口和函数返回时,调用函数和被调用函数处于不同的转换单元中,所有外部链接对象的值和通过其中可通过指针访问的所有对象的值将与抽象语义一致。 此外,在每个这样的函数输入时,被调用函数的参数值和通过其中可通过指针访问的所有对象的值将与抽象语义一致。 在这种类型的实现中,由信号函数激活的中断服务例程引用的对象将需要明确规定易失性存储,以及其他实现定义的限制。
最后,您应该注意到C99标准还说:
7.14.1.1/5“
signal
功能”如果信号的出现不是调用
abort
或raise
函数的结果,那么如果信号处理程序引用具有静态存储持续时间的任何对象,而不是通过为声明为volatile sig_atomic_t
的对象赋值,则行为是未定义的...
严格来说, running
变量可能需要声明为:
volatile sig_atomic_t running = 1;
因为对putchar()
的调用可能会改变running
的值(GCC只知道putchar()
是一个外部函数,并且不知道它做了什么 - 因为所有GCC都知道putchar()
可以调用handler()
)。
GCC可能假设对putchar
的调用可以修改任何全局变量,包括running
。
看一下pure function属性,它声明该函数对全局状态没有副作用。 我怀疑如果用一个“纯”函数调用替换putchar(),GCC将重新引入循环优化。
谢谢大家的回答和评论。 他们非常有帮助,但没有一个提供完整的故事。 [ 编辑 :迈克尔伯尔的答案现在做了,这有点多余。]我会在这里总结一下。
即使running
是静态的, handler
也不是静态的; 因此它可能会从putchar
调用并以这种方式更改running
。 由于此时不知道putchar
的实现,因此可以想象从while
循环的主体调用handler
。
假设handler
是静态的。 那么我们可以优化running
检查吗? 答案是否定的,因为signal
实现也在此编译单元之外。 对于所有gcc都知道, signal
可能会在某处存储handle
的地址(事实上,它确实存在),然后putchar
可能会通过此指针调用handler
,即使它没有直接访问该函数。
那么在什么情况下可以优化running
检查? 看起来这只有在循环体不从该转换单元外部调用任何函数时才有可能,因此在编译时可以知道在循环体内发生了什么和不发生什么。
这就解释了为什么在实践中忘记一个volatile
的东西并不像最初看起来那么重要。
putchar
可以改变running
。
理论上,只有链接时间分析才能确定它没有。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.