10

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

考虑以下不正确的 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这种情况下的值呢?

4

5 回答 5

9

在一般情况下,编译器很难准确地知道一个函数可以访问哪些对象,因此可能会对其进行修改。在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 关于抽象机器在几乎这些确切情况下的行为是这样说的:

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;
于 2010-03-25T18:57:45.747 回答
4

因为调用putchar()可能会改变running(GCC 只知道这putchar()是一个外部函数并且不知道它做了什么 - 因为所有 GCC 都知道putchar()可以调用handler())。

于 2010-03-25T18:51:11.317 回答
3

GCC 可能假设调用putchar可以修改任何全局变量,包括running.

看一下函数属性,它表明该函数对全局状态没有副作用。我怀疑如果您将 putchar() 替换为对“纯”函数的调用,GCC 将重新引入循环优化。

于 2010-03-25T18:51:59.393 回答
1

谢谢大家的回答和评论。他们非常有帮助,但没有一个提供完整的故事。[编辑:迈克尔伯尔的回答现在确实如此,这有点多余。]我会在这里总结一下。

即使running是静态的,handler也不是静态的;因此它可能会以这种方式被调用putchar并改变running。由于此时putchar不知道 的实现,因此可以想象它可以handlerwhile循环体中调用。

假设handler 静态的。那么我们可以优化running检查吗?答案是否定的,因为signal实现也在这个编译单元之外。据gcc 所知,它signal可能会存储handle某处的地址(事实上,它确实如此),然后putchar可能会handler通过此指针调用,即使它无法直接访问该函数。

那么在什么情况下可以优化running检查呢?似乎只有在循环体不从该翻译单元外部调用任何函数的情况下才有可能,以便在编译时知道循环体内部发生了什么和没有发生什么。

这就解释了为什么忘记 avolatile在实践中并不像起初看起来那么重要。

于 2010-03-25T19:42:32.353 回答
1

putchar 可以改变running

理论上,只有链接时间分析可以确定它没有。

于 2011-10-27T02:19:58.723 回答