GCC LTO是否执行跨文件死代码消除?



假设我有一个函数

void do_something() {
//....
#ifdef FEATURE_X
feature_x();
#endif
//....
}

我可以毫无问题地编译和运行它;如果我想要这个功能,我可以传递-D FEATURE_X并且它可以工作。

但是,如果我想将do_something放入另一个文件中(并且不必在每次我决定更改选项时重新编译该文件)。 如果它在同一文件中,我假设

const int FEATURE_X=0;
void do_something() {
//....
if(FEATURE_X) {
feature_x();
}
//....
}

将正确使用死代码消除,消除调用。 如果我把它放在另一个文件中,没有 LTO,

extern const int FEATURE_X;
void do_something() {
//....
if(FEATURE_X) {
feature_x();
}
//....
}

它不会删除代码(它无法知道)。 那么,启用链接时间优化后,编译器是否可以在链接时检测FEATURE_X的值,确定代码是否被使用,并在适当时将其删除?

GCC 确实会跨模块删除无法访问的函数,但它将无法确定代码在上一个测试用例中是否失效,因为 FEATURE_X 的常量值确定为时已晚。

如果您将使用 -D 方式或将const int FEATURE_X=0;放入每个模块中,那么是的,代码将被消除。

显示 LTO 导致死代码消除的示例

测试设置:

notmain.c

int notmain(int i) {
return i + 1;
}
int notmain2(int i) {
return i + 2;
}

主.c

int notmain(int);
int main(int argc, char **argv) {
return notmain(argc);
}

无 LTO 的对照实验

在没有 LTO 的情况下编译和反汇编:

gcc -O3 -c notmain.c
gcc -O3 notmain.o main.c
objdump -d a.out

输出包含:

0000000000001040 <main>:
1040:       f3 0f 1e fa             endbr64
1044:       e9 f7 00 00 00          jmp    1140 <notmain>
1049:       0f 1f 80 00 00 00 00    nopl   0x0(%rax)
0000000000001140 <notmain>:
1140:       f3 0f 1e fa             endbr64
1144:       8d 47 01                lea    0x1(%rdi),%eax
1147:       c3                      ret
1148:       0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
114f:       00
0000000000001150 <notmain2>:
1150:       f3 0f 1e fa             endbr64
1154:       8d 47 02                lea    0x2(%rdi),%eax
1157:       c3                      ret

所以没有删除无用的notmain2

我们还可以查看对象大小:

size a.out

其中输出:

text    data     bss     dec     hex filename
1304     544       8    1856     740 a.out

此外,作为奖励,我们注意到函数调用没有内联:

0000000000001040 <main>:
1040:       f3 0f 1e fa             endbr64
1044:       e9 f7 00 00 00          jmp    1140 <notmain>
1049:       0f 1f 80 00 00 00 00    nopl   0x0(%rax)

观察 LTO 执行 DCE

gcc -c -flto -O3 notmain.c
gcc -flto -O3 notmain.o main.c
objdump -d a.out

输出不包含notmain符号,不包含notmain2符号。所有内容都完全内联到main中,在单个指令中将 1 添加到第一个参数 rdi,并将其放入返回寄存器 eax:

0000000000001040 <main>:
1040:       f3 0f 1e fa             endbr64
1044:       8d 47 01                lea    0x1(%rdi),%eax
1047:       c3                      ret
1048:       0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)

内联还提到:链接时间优化和内联

美。检查尺寸:

size a.out

输出:

text    data     bss     dec     hex filename
1217     544       8    1769     6e9 a.out

我们看到,由于内联和死代码消除,文本大小根据需要减小。

LTO 执行 DCE,即使内联未发生

在上面的示例中,不清楚函数 DCE 消除是否仅在涉及内联时才发生。因此,让我们用以下方法进行测试:

int __attribute__ ((noinline)) notmain(int i) {
return i + 1;
}

编译和反汇编:

gcc -c -flto -O3 notmain.c
gcc -flto -O3 notmain.o main.c
objdump -d a.out

输出包含:

0000000000001040 <main>:
1040:       f3 0f 1e fa             endbr64
1044:       e9 f7 00 00 00          jmp    1140 <notmain>
1049:       0f 1f 80 00 00 00 00    nopl   0x0(%rax)
0000000000001140 <notmain>:
1140:       8d 47 01                lea    0x1(%rdi),%eax
1143:       c3                      ret

没有notmain2.因此,即使没有删除无用notmain2notmain也被删除了。

使用-O0编译notmain.c时不会发生函数删除

我不明白为什么:为什么 GCC 在使用 -O0 编译目标文件时不使用 LTO 进行功能死代码消除?

在 Ubuntu 23.04 amd64、GCC 12.2.0 上测试。

相关内容

  • 没有找到相关文章

最新更新