这个问题主要是学术性的。我出于好奇而问,不是因为这给我带来了实际问题。
考虑以下错误的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
的值?
答案 0 :(得分:9)
在一般情况下,编译器很难确切知道函数可能访问哪些对象,因此可能会进行修改。在调用putchar()
时,GCC不知道是否可能有putchar()
实现可以修改running
,因此它必须有点悲观,并假设{ {1}}实际上可能已被更改。
例如,稍后在翻译单元中可能会有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
访问它(假设是这种情况),并且编译器可以返回并“修复”while循环中的悲观化。
击>
由于putchar()
是静态的,编译器可能会确定无法从翻译单元外部访问它并进行您正在讨论的优化。但是,由于可通过running
访问并且handler()
可从外部访问,因此编译器无法优化访问。即使你使handler()
为静态,也可以从外部访问它,因为你将它的地址传递给另一个函数。
请注意,在您的第一个示例中,即使我在上一段中提到的内容仍然是正确的,编译器也可以优化对handler()
的访问,因为“抽象机器模型”C语言基于“不” t考虑异步活动,除非在非常有限的情况下(其中一个是running
关键字,另一个是信号处理,尽管信号处理的要求不够强,无法阻止编译器进行优化在第一个示例中访问volatile
。
事实上,在这些确切的情况下,这是C99关于抽象机器行为的内容:
5.1.2.3/8“程序执行”
示例1:
实现可以定义抽象语义和实际语义之间的一对一对应关系:在每个序列点,实际对象的值将与抽象语义指定的值一致。关键字
running
将是多余的。或者,实现可以在每个转换单元内执行各种优化,使得实际语义仅在跨转换单元边界进行函数调用时才与抽象语义一致。在这样的实现中,在每个函数入口和函数返回时,调用函数和被调用函数处于不同的转换单元中,所有外部链接对象的值和通过其中可通过指针访问的所有对象的值将与抽象语义一致。此外,在每个这样的函数输入时,被调用函数的参数值和通过其中可通过指针访问的所有对象的值将与抽象语义一致。在这种类型的实现中,由信号函数激活的中断服务例程引用的对象需要明确规定易失性存储,以及其他实现定义的限制。
最后,您应该注意到C99标准也说:
7.14.1.1/5“
volatile
功能如果信号的出现不是调用
signal
或abort
函数的结果,那么如果信号处理程序引用具有静态存储持续时间的任何对象而不是通过赋值,则行为是未定义的到声明为raise
的对象......
严格来说,volatile sig_atomic_t
变量可能需要声明为:
running
答案 1 :(得分:4)
因为对putchar()
的调用可能会更改running
的值(GCC只知道putchar()
是一个外部函数,并且不知道它做了什么 - 因为所有GCC都知道{{ 1}}可以调用putchar()
)。
答案 2 :(得分:3)
GCC可能假设对putchar
的调用可以修改任何全局变量,包括running
。
查看pure函数属性,该属性声明该函数对全局状态没有副作用。我怀疑如果用一个“纯”函数调用替换putchar(),GCC将重新引入循环优化。
答案 3 :(得分:1)
谢谢大家的回答和评论。他们非常有帮助,但没有一个提供完整的故事。 [编辑:Michael Burr的答案现在确实如此,这有点多余。]我会在这里总结一下。
即使running
是静态的,handler
也不是静态的;因此可以从putchar
调用它并以这种方式更改running
。由于此时不知道putchar
的实现,因此可以想象从handler
循环的主体调用while
。
假设handler
是静态的。我们可以优化running
检查吗?答案是否定的,因为signal
实现也在此编译单元之外。对于所有gcc都知道,signal
可能会在某个地方存储handle
的地址(事实上,它确实存在),然后putchar
可以通过此指针调用handler
,即使它没有直接访问该功能。
那么在什么情况下可以将running
检查优化掉?似乎这只有在循环体不从这个转换单元外部调用任何函数时才有可能,因此在编译时知道它在循环体内是什么和不发生。
这就解释了为什么忘记volatile
在实践中并不像最初看起来那么重要。
答案 4 :(得分:1)
putchar
可以更改running
。
理论上,只有链接时间分析可以确定它没有。