这个问题主要是学术性的。我出于好奇而问,不是因为这对我构成了实际问题。
请考虑以下不正确的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
。但是,可以说程序员忘记了这一点。带有
-O3
标志的gcc 4.3.3将循环体(在对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不知道是否存在可以修改putchar()
的running
实现,因此它必须有些悲观,并假设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几乎在以下确切情况下说明了抽象机的行为:
最后,您应该注意,C99标准还指出:
因此严格来说,
running
变量可能需要声明为:volatile sig_atomic_t running = 1;
关于c - 为什么gcc不删除对非 volatile 变量的检查?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/2518430/