Tho*_*mas 10 c optimization gcc volatile
这个问题主要是学术问题.我出于好奇而不是因为这给我带来了实际问题.
考虑以下不正确的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;
}
Run Code Online (Sandbox Code Playgroud)
此程序不正确,因为处理程序中断了程序流,因此running可以随时修改,因此应该声明volatile.但是,让我们说程序员忘记了这一点.
gcc 4.3.3,带有-O3标志,将循环体(在对running标志进行一次初始检查后)编译为无限循环
.L7:
jmp .L7
Run Code Online (Sandbox Code Playgroud)
这是可以预料的.
现在我们在while循环中放入一些微不足道的东西,比如:
while (running)
putchar('.');
Run Code Online (Sandbox Code Playgroud)
突然间,gcc不再优化循环条件了!循环体的组件现在看起来像这样(再次-O3):
.L7:
movq stdout(%rip), %rsi
movl $46, %edi
call _IO_putc
movl running(%rip), %eax
testl %eax, %eax
jne .L7
Run Code Online (Sandbox Code Playgroud)
我们看到running每次循环都会从内存中重新加载; 它甚至没有缓存在寄存器中.显然gcc现在认为价值running可能会发生变化.
那么为什么gcc突然决定需要重新检查running这种情况下的价值呢?
在一般情况下,编译器很难确切地知道函数可能访问哪些对象,因此可能会修改.在putchar()被调用的地方,GCC不知道是否putchar()可能存在可以修改的实现,running因此它必须有些悲观并且假设running事实上可能已经改变了.
例如,putchar()稍后翻译单元中可能会有一个实现:
int putchar( int c)
{
running = c;
return c;
}
Run Code Online (Sandbox Code Playgroud)
即使putchar()翻译单元中没有实现,也可能存在某些内容,例如,传递running对象的地址,putchar以便可以修改它:
void foo(void)
{
set_putchar_status_location( &running);
}
Run Code Online (Sandbox Code Playgroud)
请注意,您的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功能"如果信号不是作为调用
abortorraise函数的结果而发生的,那么如果信号处理程序引用具有静态存储持续时间的任何对象,而不是通过为声明为volatile sig_atomic_t... 的对象赋值,则行为是未定义的.
严格来说,running变量可能需要声明为:
volatile sig_atomic_t running = 1;
Run Code Online (Sandbox Code Playgroud)