【发布时间】:2022-01-14 04:11:44
【问题描述】:
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 在为负时可能会通过 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
【问题讨论】:
-
或许应该在 GCC 的 bugzilla 上报告这个问题,gcc.gnu.org/bugzilla,使用关键字“missed-optimization”
-
如果将
else if (a == 9)更改为简单的else会发生什么? -
1.代码不一样。 2. 这种行为在 11.xx 版本中有所改变。在 11 之前,生成的代码完全相同。 3. 你真的测试了执行时间吗?指令数量不一定是最佳性能指标。
-Os使生成的代码相同 -
@0___________ 我确实检查了执行时间,
f1始终较慢。-Os不会使生成的代码在我的 gcc 版本中相同。不必要的jmp、ret和movq、addq、movq序列仍然存在。 -
clang 能够发现它可以使用查找表作为加数,并避免效率较低的跳转表:godbolt.org/z/vbv7vdWc9 尽管即使使用
__builtin_unreachable(),它也没有注意到它可以跳过范围检查。 (如果它能够确定表条目可以是 8 位而不是 64 位并加载零扩展名,那就太好了 - 将节省 70 个字节。)无论如何,如果你真的需要优化这样的东西,你可能想要首先将其写为查找表。
标签: c assembly gcc compiler-optimization unreachable-code