为何GCC与Clang保留结构体常量函数指针调用却优化全局指针调用?
原C代码
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; }
编译生成的汇编代码(GCC 13.2 x86-64)
使用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调用函数t时会生成call指令,而通过全局常量函数指针myt调用时却无对应指令? - 既然
f为静态分配且编译时初始化,其成员t是不可修改的常量指针(修改会触发未定义行为),指向空函数t,为何编译器无法完全消除该无意义的函数调用?
解答
1. 全局函数指针myt的调用被优化的原因
全局常量函数指针myt被声明为void (*const myt)(struct foo *f) = t;,这里的const直接限定指针本身为只读,编译器可以确定它的值在整个程序生命周期内不会被修改(任何修改都是未定义行为,编译器无需考虑这种非法场景)。因此编译myt(&f);时,编译器能直接将其解析为对t的调用,而t是空函数,除了ret没有任何实际操作,编译器进一步优化掉了这个无意义的调用,所以汇编里看不到对应的call指令。
2. 结构体成员t的调用未被优化的原因
虽然结构体foo的成员t被声明为void (*const t)(struct foo *f);,且全局变量f是编译时初始化的,但这里的const只是结构体成员的属性,而非全局变量f本身的属性——f本身不是const限定的全局变量,你可以合法修改f的其他成员(如果存在),只是修改f.t会触发未定义行为。
编译器优化时,默认会假设全局变量可能被其他编译单元的代码修改(除非用static限定,或开启更激进的优化选项并配合全局const声明)。即使f.t是成员级的const,编译器无法确定其他代码是否会通过未定义行为修改它的值,因此不能直接将f.t(&f);替换为对t的直接调用,更无法直接消除这个调用。
如果把f声明为const foo f = {.t = t};,给全局变量本身加上const限定,编译器就能确定f的所有成员都不会被修改,此时会把f.t(&f);的调用也优化掉,汇编里就不会出现对应的call指令了。
内容的提问来源于stack exchange,提问作者user1806687

