这个问题主要是学术性的。我出于好奇而问,不是因为这对我构成了实际问题。

请考虑以下不正确的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/

10-09 01:52