【发布时间】:2020-05-14 11:19:51
【问题描述】:
我向一位同事声称,if (i < input.size() - 1) print(0); 将在此循环中得到优化,因此不会在每次迭代中读取input.size(),但事实证明并非如此!
void print(int x) {
std::cout << x << std::endl;
}
void print_list(const std::vector<int>& input) {
int i = 0;
for (size_t i = 0; i < input.size(); i++) {
print(input[i]);
if (i < input.size() - 1) print(0);
}
}
根据Compiler Explorer 和gcc 选项-O3 -fno-exceptions,我们实际上是在每次迭代读取input.size() 并使用lea 执行减法!
movq 0(%rbp), %rdx
movq 8(%rbp), %rax
subq %rdx, %rax
sarq $2, %rax
leaq -1(%rax), %rcx
cmpq %rbx, %rcx
ja .L35
addq $1, %rbx
有趣的是,在 Rust 中确实发生了这种优化。看起来i 被替换为变量j,每次迭代都会递减,而测试i < input.size() - 1 被替换为j > 0。
fn print(x: i32) {
println!("{}", x);
}
pub fn print_list(xs: &Vec<i32>) {
for (i, x) in xs.iter().enumerate() {
print(*x);
if i < xs.len() - 1 {
print(0);
}
}
}
在Compiler Explorer 中,相关程序集如下所示:
cmpq %r12, %rbx
jae .LBB0_4
我检查了,我很确定 r12 是 xs.len() - 1 和 rbx 是计数器。早些时候有一个add 用于rbx 和一个mov 在循环之外进入r12。
这是为什么?看起来如果 GCC 能够像它那样内联 size() 和 operator[],它应该能够知道 size() 不会改变。但也许 GCC 的优化器判断不值得将其拉出到变量中?或者也许还有其他一些可能的副作用会导致这不安全——有人知道吗?
【问题讨论】:
-
另外
println可能是一个复杂的方法,编译器可能无法证明println不会改变向量。 -
@MooingDuck:另一个线程将是数据竞赛 UB。编译器可以并且确实假设 不会发生。这里的问题是对
cout.operator<<()的非内联函数调用。编译器不知道这个黑盒函数没有从全局中获得对std::vector的引用。 -
@PeterCordes:你说得对,其他线程不是独立的解释,
println或operator<<的复杂性是关键。 -
编译器不知道这些外部方法的语义。