我正在查看 Rust 编译器的输出程序集,注意到它并没有像我预期的那样删除一些边界检查。经过额外的实验,我发现这个问题似乎与 LLVM 有关(Rust 使用的编译器/优化器后端)。为了简单起见,我创建了我之前看到的简化 C 版本。在此示例中,您可以看到不可能到达
printf
语句,因为 a
和 b
之和始终大于或等于 a
。
void foo(uint8_t a, uint8_t b, uint64_t c) {
if (((uint64_t) a) + ((uint64_t) b) >= c) {
return;
}
if ((uint64_t) a >= c) {
// Do something
printf("foo");
}
}
令我惊讶的是,LLVM(x86-64 clang 17.0.1 带有
-O3
;Godbolt)并没有优化掉 printf
。然而,GCC(带有 -O3
的 x86-64 GCC 13.2;Godbolt)能够正确确定整个函数可以优化为单个 ret
指令。
话虽这么说,我给出的示例试图通过选择使整数溢出不可能发生的类型来对编译器更加慷慨,并将比较与单个 LLVM IR 基本块隔离。举一个更常见的例子,假设所有 3 个变量都是
size_t
。由于 C 允许无符号整数溢出,因此简单地将上面的示例转换为 size_t
将导致 printf
实际上是可达的。相反,让我们添加一些先决条件,在执行前面的代码之前对 a
和 b
进行限制。
void foo(size_t a, size_t b, size_t c) {
// Some pre-condition that puts limits on a and b
if (a > 2048 || b > 1024) {
// Using __builtin_unreachable() should have the same effect
return;
}
if (a + b >= c) {
return;
}
if (a >= c) {
printf("foo");
}
}
GCC 再次能够将所有内容优化为单个
ret
,但 clang 无法删除 printf
(Godbolt)。在这种情况下,LLVM 需要跨多个非循环基本块执行范围分析,但我希望像 LLVM 这样的主要编译器能够轻松处理上面的代码。
为什么 LLVM 不运行优化过程来进行整数值的简单范围分析?
进入这个阶段,我没想到它能够在循环内执行复杂的范围分析。然而,似乎很奇怪的是,它并没有在单个基本块的范围内进行如此简单的分析。就 LLVM 所做的所有工作而言,我发现几乎没有人考虑过执行此类优化。
根据@PeterCordes的建议,我在LLVM论坛上问了这个问题。
他们的回应是,这是
ConstraintElimination
阶段中的一个错误,他们将尝试在以后的版本中修复。最可能的原因是它没有显式添加谓词来表示无符号值始终大于或等于零。