[英]Why is gcc emitting worse code with __builtin_unreachable?
f0
和f1
如下,
long long b;
void f0(int a) {
a %= 10;
if (a == 0) b += 11;
else if (a == 1) b += 13;
else if (a == 2) b += 17;
else if (a == 3) b += 19;
else if (a == 4) b += 23;
else if (a == 5) b += 29;
else if (a == 6) b += 31;
else if (a == 7) b += 37;
else if (a == 8) b += 41;
else if (a == 9) b += 43;
}
void f1(int a) {
a %= 10;
if (a == 0) b += 11;
else if (a == 1) b += 13;
else if (a == 2) b += 17;
else if (a == 3) b += 19;
else if (a == 4) b += 23;
else if (a == 5) b += 29;
else if (a == 6) b += 31;
else if (a == 7) b += 37;
else if (a == 8) b += 41;
else if (a == 9) b += 43;
else __builtin_unreachable();
}
假设参数a
在程序中始终为正,编译器应该为f1
生成更优化的代码,因为在f0
中,当 a 为负时, a
可能会通过 if-else 块,因此编译器应该生成默认的“什么都不做并返回“ 代码。 但是在f1
中, a
的可能范围用__builtin_unreachable
清楚地说明,这样编译器就不必考虑a
何时超出范围。
不过f1
实际上跑得比较慢,所以我看了一下反汇编。 这是f0
的控制流部分。
jne .L2
addq $11, b(%rip)
ret
.p2align 4,,10
.p2align 3
.L2:
cmpl $9, %eax
ja .L1
movl %eax, %eax
jmp *.L5(,%rax,8)
.section .rodata
.align 8
.align 4
.L5:
.quad .L1
.quad .L13
.quad .L12
.quad .L11
.quad .L10
.quad .L9
.quad .L8
.quad .L7
.quad .L6
.quad .L4
.text
.p2align 4,,10
.p2align 3
.L4:
addq $43, b(%rip)
.L1:
ret
.p2align 4,,10
.p2align 3
.L6:
addq $41, b(%rip)
ret
.p2align 4,,10
.p2align 3
...
gcc 巧妙地将 if-else 块转换为跳转表,并将默认情况L1
放在L4
中以节省空间。
现在看看反汇编的f1
的整个控制流程。
jne .L42
movq b(%rip), %rax
addq $11, %rax
.L43:
movq %rax, b(%rip)
ret
.p2align 4,,10
.p2align 3
.L42:
movl %eax, %eax
jmp *.L46(,%rax,8)
.section .rodata
.align 8
.align 4
.L46:
.quad .L45
.quad .L54
.quad .L53
.quad .L52
.quad .L51
.quad .L50
.quad .L49
.quad .L48
.quad .L47
.quad .L45
.text
.p2align 4,,10
.p2align 3
.L47:
movq b(%rip), %rax
addq $41, %rax
jmp .L43
.p2align 4,,10
.p2align 3
.L48:
movq b(%rip), %rax
addq $37, %rax
jmp .L43
.p2align 4,,10
.p2align 3
.L49:
movq b(%rip), %rax
addq $31, %rax
jmp .L43
.p2align 4,,10
.p2align 3
.L50:
movq b(%rip), %rax
addq $29, %rax
jmp .L43
.p2align 4,,10
.p2align 3
.L51:
movq b(%rip), %rax
addq $23, %rax
jmp .L43
.p2align 4,,10
.p2align 3
.L52:
movq b(%rip), %rax
addq $19, %rax
jmp .L43
.p2align 4,,10
.p2align 3
.L53:
movq b(%rip), %rax
addq $17, %rax
jmp .L43
.p2align 4,,10
.p2align 3
.L54:
movq b(%rip), %rax
addq $13, %rax
jmp .L43
.p2align 4,,10
.p2align 3
.L45:
movq b(%rip), %rax
addq $43, %rax
jmp .L43
是的 gcc 确实捕获了__builtin_unreachable
,但是由于某种原因,每次返回之前都有一个不必要的跳转,并且跳转表有L45
的重复条目。 此外,它不是简单地addq $N, b(%rip)
,而是在返回之前一直写movq b(%rip), %rax
, addq $N, %rax
,然后是movq %rax, b(%rip)
。
是什么让 gcc 产生明显愚蠢的代码?
二进制文件是在 Fedora Linux 下用-O3
编译的,我使用的 gcc 版本是11.2.1 20211203
这是我能想到的最好的解释。
编译器显然可以(至少在某种程度上)进行优化,其中可以提升 if/else 树的所有分支共有的代码。 但是在f0
版本中,无法应用这种优化,因为“默认”情况根本没有代码,特别是既不加载也不存储b
。 因此,编译器只是尽可能地单独优化案例,将每个案例保留为单个 RMW 添加内存指令。
在f1
版本中,您的__builtin_unreachable
已删除默认分支。 因此,现在每个分支在概念上都包含b
的加载、某个常量的添加以及返回到b
的存储。 编译器似乎注意到它们都有共同的存储,因此将其吊出。 不幸的是,这实际上导致整体代码更差,因为现在个别情况不能使用 RMW 添加; 他们必须进行加载并作为单独的说明添加。 此外,案件不能再ret
解决; 他们都必须跳到吊出的商店。 并且编译器不知何故没有意识到负载可以被提升,所以它在所有情况下都不必要地重复了。
我猜问题的一部分是提升是在一个独立于目标的传递中完成的,它将加载、添加和存储视为独立的操作。 如果它们保持在一起,那么稍后一些特定于目标的窥视孔通道可能会将它们组合成单个添加内存指令; 但较早的传球似乎没有考虑将它们放在一起可能是有利的,并认为任何吊装都必须是好的。 在 RISC 类型的加载/存储机器上,RMW 总是必须是三个指令,仅提升存储可能仍然有些帮助,但对于 x86 绝对不是。
所以这可能是两个单独的错过优化问题。 第一个是没有注意到负载可以被提升(或者可能注意到但决定不这样做),这似乎是一个明显的错误。 第二个是没有正确评估提升是否值得额外跳跃的成本,这可能更多的是应用在这种情况下碰巧错误的启发式方法。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.