C编译器优化-如何在模块之间完成?

dkqlctbz  于 2023-06-21  发布在  其他
关注(0)|答案(1)|浏览(83)

假设2源文件名为file1.cfile2.c。它们都由编译器单独编译,并在最后一步链接。考虑优化GCC -Ofast。它们都有其对应的头文件,并且头文件包括彼此。
文件1:

//#include...

int bool_check;
int main(void) {
    bool_check = 1;    
    while (bool_check) {
        do_something(); //Defined in another file
    }
}

文件2:

void do_something() {
    extern int bool_check;
    bool_check = 0;
}

在本例中,file1调用file2中的一个函数。

  • 是否允许编译器/优化器优化file1中的代码,说bool_check永远不会改变,所以我们不需要执行常量从内存读取操作?
  • 如果没有,如果文件是单独编译的,它如何知道file2中的函数修改了变量?
  • 如果是,这不是一个编译器bug吗?

这不是一个很好的真实用例,但很适合演示。

5cnsuln7

5cnsuln71#

是否允许编译器/优化器优化file 1中的代码,说bool_check永远不会改变,所以我们不需要执行常量从内存读取操作?
不能。只有当它能证明bool_check实际上没有被do_something()的调用修改时,它才能进行优化,而这只有在它能看到代码的情况下才有可能。
如果不是,如果文件是单独编译的,它怎么知道file 2中的函数修改了变量?
一般来说,它不知道。因此,它必须假设do_something()可能修改bool_check,这意味着它必须在每次调用后从内存中重新加载它。
一般来说,如果编译器看不到被调用函数的代码,它必须假设该函数可能读取或写入程序中的 * 每个 * 全局变量,以及每个静态,局部或动态对象,其地址已“转义”并且可能被调用函数所知。因此,在调用之前,必须将所有之前对此类对象的写入存储到内存中,并且必须在调用之后加载所有后续读取。
例如:

void other1(int *p);
void other2(void);

int foo() {
    int x, y, z;
    int *p = &z;
    other1(&x);
    x = 3;
    y = 5;
    *p = 7;
    other2();
    return x + y;
}

在调用other2()之后,编译器必须从内存中重新加载x。对other1()的调用可能将指向x的指针隐藏在other2()也可以访问的某个共享对象中,在这种情况下,other2()可能通过该指针修改了x。同样,x=3必须将值3实际存储到内存中,并且不能被优化掉。
另一方面,y=5不需要存储到内存中,并且y不需要在other2()之后重新加载; y的地址从未被取过,因此没有其他代码可以“知道”它的位置以修改它。实际上,y根本不需要占用内存;它可以简单地被return表达式中的立即常数替换。
z也是如此;即使它的地址已经被取,它也没有被传递到函数foo之外,因此它没有“转义”。这个逻辑称为escape analysis。所以编译器可以在不改变程序的可观察行为的情况下优化它。
话虽如此,现代工具链能够 * 链接时优化 *,其中编译器不仅为每个源文件(翻译单元)发出汇编代码,而且还发出一些更详细地描述代码语义的中间表示(IR)。所有这些信息都可供链接器使用,链接器可以重新运行优化和代码生成,现在整个程序立即“可见”。此时,优化器可以看到调用do_something()修改了bool_check,尽管这并没有真正改变任何东西,因为编译器已经假设它是这样的。更重要的是,优化器可以看到哪些变量没有被修改,并优化掉任何死存储或不必要的重新加载。

相关问题