gcc和clang发出看似冗余的函数调用

moiiocjp  于 2023-10-16  发布在  其他
关注(0)|答案(1)|浏览(99)
typedef struct foo
{
    void (*const t)(struct foo *f);
} foo;

void t(struct foo *f)
{
    
}

void (*const myt)(struct foo *f) = t;

foo f = {.t = t};

int main(void)
{
    f.t(&f);
    myt(&f);

    return 0;
}

当用x86-64 gcc 13.2和clang 16.0.0编译上述代码时,会生成类似的汇编代码。下面显示的是gcc输出。

t:
        ret
main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:f
        call    [QWORD PTR f[rip]]
        xor     eax, eax
        add     rsp, 8
        ret
f:
        .quad   t
myt:
        .quad   t

为什么两个编译器在通过struct foo调用时都发出对t函数的调用,而在通过myt函数指针调用时却没有?为什么编译器不能看到struct foo中的t函数指针指向一个空函数,并一起消除调用?由于f是静态分配的,并在编译时初始化,因此它的成员t,一个在运行时不能改变的常量指针(不调用未定义的行为),指向一个空函数t

tzdcorbm

tzdcorbm1#

**GCC和Clang都提供了在main**之前运行任意代码的机制,因此这些编译器必须尊重这种可能性。(包括使用静态构造函数或GNU C __attribute__((constructor))链接C++库。
全局变量foo f不是const,因此GCC和clang假设它可能在执行到达f.t(&f);之前被修改。**更改为const foo f = ...;**让GCC和clang知道函数指针值是编译时常量,并通过它内联并优化空函数。(神箭

那么修改f.t的代码看起来会是什么样子呢?
显然它不能执行f.t = whatever,因为.t成员是const。GCC和clang也拒绝编译f = (foo){NULL};来替换整个结构体的值,抱怨const成员。(Clang says error:无法分配给具有常量限定数据成员“t” 的变量“f”。GCC使用与const foo f相同的错误,error:指定只读变量'f'

但他们确实允许memcpy没有警告甚至从-Wall -Wextra,与如果fconst,则会发出警告。(我使用NULL作为占位符,只是为了避免编写另一个有效的函数。如果你可以将它改为NULL,你可以将它改为一个在调用时会做一些事情的函数。

#include <string.h>
// Code like this could hypothetically run before main in another compilation unit
void startup(void){
    // f = (foo){NULL};    // illegal

    foo tmp = {NULL};
    memcpy(&f, &tmp, sizeof(f));  // legal, it seems, as long as foo f isn't const foo f
      //   (In which case this warns, and will segfault at run-time)
      // actually clang emits no instructions for the memcpy in the UB case where it's trying to write const foo f.
}

我没有检查过ISO标准,但假设GCC/clang的(缺少)警告与其优化器假设其他代码可能做的事情一致,这就解释了为什么在这种情况下非constf不被视为具有已知值。

为什么static foo f没有帮助?

将其设置为static foo f(在一个没有定义startup()函数来修改它的编译单元中)应该允许常量传播,因为编译单元之外的任何东西都看不到它,内部也没有任何东西改变它的值。
但是,即使gcc -fwhole-program和GCC/clang -flto(反汇编链接的二进制文件)也无法内联f.t(&f),即使这些选项确实允许它们内联myt(&f)(非conststaticmyt)。(Godbolt).
更改对f.t(0);myt(0)的调用允许GCC和clang优化这两个调用!(使用static foo fconst myt,或者使用-fwhole-program-flto。)我认为将&f作为arg传递会破坏转义分析,尽管编译器在理论上可以证明调用目标仍然总是在这个编译单元中定义的t(),这实际上并没有让地址转义到编译单元之外。

// or non-const is the same if we're using -flto or -fwhole-program
void (*const myt)(const struct foo *f) = t;
static foo f = {.t = t};

int main(void)
{
    f.t(0);    // not f.t(&f) so it's clear to the optimizer the address of f doesn't escape
    myt(0);

// f.t(0); myt(&f);  // lets clang -flto optimize away both calls, but still not GCC -fwhole-program
}

在C中,main从程序内部再次调用不是UB。因此,如果f.t(&f)更改了f.t,则main的下一次执行(例如,来自另一个编译单元中的__attribute__((destructor))函数)将是对不同函数的调用。这在这里实际上是不可能的,但是很容易想象编译器可能在试图证明这一点的过程中陷入了循环,然后放弃了。(f的地址被这个编译单元中的一个非static函数获取并传递给一个函数调用,这通常意味着地址转义。看到它转到t()而不改变f,需要在它完成转义分析之前进行内联,但是它不能通过函数指针内联,除非转义分析可以证明它知道哪个函数被调用。
另一方面,myt是真正的const,所以很容易优化。即使我们不将其设置为conststatic,对于-flto-fwhole-program,编译器也很容易看到这个编译单元(或任何具有LTO的编译单元)中没有任何东西改变它,并且&myt没有作为函数arg传递,因此显然不会转义。(如果没有-fltoconst/static,constprop到全局myt也不会发生。
Godbolt -使用main执行f.t(0);myt(0);*const mytstatic foo f足以让GCC和clang优化这两个调用,即使没有-flto-fwhole-program。除非这个编译单元定义了一个像startup()这样的函数,它将memcpy转换为&f
但是对于非staticfoo f,这种优化再次是不可能的,因为它甚至可以在第一次/唯一一次执行main之前被修改。
顺便说一句,我的Godbolt链接有-fvisibility=hidden只是为了确保符号插入是不可能的,这将是另一种方式,一个非static变量的值不同于源代码中的初始化器。但这对于主可执行文件中的全局变量来说已经不可能了,只与-fPIC相关,以编译对共享库安全的代码。

相关问题