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
。
1条答案
按热度按时间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
,与如果f
是const
,则会发出警告。(我使用NULL
作为占位符,只是为了避免编写另一个有效的函数。如果你可以将它改为NULL,你可以将它改为一个在调用时会做一些事情的函数。我没有检查过ISO标准,但假设GCC/clang的(缺少)警告与其优化器假设其他代码可能做的事情一致,这就解释了为什么在这种情况下非
const
f
不被视为具有已知值。为什么
static foo f
没有帮助?将其设置为
static foo f
(在一个没有定义startup()
函数来修改它的编译单元中)应该允许常量传播,因为编译单元之外的任何东西都看不到它,内部也没有任何东西改变它的值。但是,即使
gcc -fwhole-program
和GCC/clang-flto
(反汇编链接的二进制文件)也无法内联f.t(&f)
,即使这些选项确实允许它们内联myt(&f)
(非const
非static
myt
)。(Godbolt).更改对
f.t(0);
和myt(0)
的调用允许GCC和clang优化这两个调用!(使用static foo f
和const myt
,或者使用-fwhole-program
或-flto
。)我认为将&f
作为arg传递会破坏转义分析,尽管编译器在理论上可以证明调用目标仍然总是在这个编译单元中定义的t()
,这实际上并没有让地址转义到编译单元之外。在C中,
main
从程序内部再次调用不是UB。因此,如果f.t(&f)
更改了f.t
,则main
的下一次执行(例如,来自另一个编译单元中的__attribute__((destructor))
函数)将是对不同函数的调用。这在这里实际上是不可能的,但是很容易想象编译器可能在试图证明这一点的过程中陷入了循环,然后放弃了。(f
的地址被这个编译单元中的一个非static
函数获取并传递给一个函数调用,这通常意味着地址转义。看到它转到t()
而不改变f
,需要在它完成转义分析之前进行内联,但是它不能通过函数指针内联,除非转义分析可以证明它知道哪个函数被调用。另一方面,
myt
是真正的const
,所以很容易优化。即使我们不将其设置为const
或static
,对于-flto
或-fwhole-program
,编译器也很容易看到这个编译单元(或任何具有LTO的编译单元)中没有任何东西改变它,并且&myt
没有作为函数arg传递,因此显然不会转义。(如果没有-flto
或const
/static
,constprop到全局myt
也不会发生。Godbolt -使用
main
执行f.t(0);myt(0);
,*const myt
和static foo f
足以让GCC和clang优化这两个调用,即使没有-flto
或-fwhole-program
。除非这个编译单元定义了一个像startup()
这样的函数,它将memcpy
转换为&f
。但是对于非
static
foo f
,这种优化再次是不可能的,因为它甚至可以在第一次/唯一一次执行main
之前被修改。顺便说一句,我的Godbolt链接有
-fvisibility=hidden
只是为了确保符号插入是不可能的,这将是另一种方式,一个非static
变量的值不同于源代码中的初始化器。但这对于主可执行文件中的全局变量来说已经不可能了,只与-fPIC
相关,以编译对共享库安全的代码。