为什么 GCC 不能假设 std::vector::size 在这个循环中不会改变?

Why can't GCC assume that std::vector::size won't change in this loop?

本文关键字:循环 改变 不能 GCC 假设 std size vector 为什么      更新时间:2023-10-16

我向一位同事声称,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);
}
}

根据带有 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);
}
}
}

在编译器资源管理器中,相关程序集如下所示:

cmpq    %r12, %rbx
jae     .LBB0_4

我检查了一下,我很确定r12xs.len() - 1rbx是计数器。早些时候有一个用于rbxadd和一个循环外进入r12mov

这是为什么呢?似乎如果 GCC 能够像它一样内联size()operator[],它应该能够知道size()不会改变。但也许 GCC 的优化器判断不值得将其拉到变量中?或者也许还有其他一些可能的副作用会使这不安全 - 有人知道吗?

cout.operator<<(int)的非内联函数调用是优化器的黑盒(因为库只是用C++编写的,优化器看到的只是一个原型;请参阅注释中的讨论(。 它必须假设全局变量可能指向的任何内存都已被修改。

(或std::endl呼叫。 顺便说一句,为什么要在这一点上强制冲洗cout,而不仅仅是打印'n'

例如,就其所知,std::vector<int> &input是对全局变量的引用,其中一个函数调用修改了该全局变量。 (或者某处有一个全局vector<int> *ptr,或者有一个函数返回指向其他编译单元中static vector<int>的指针,或者以其他方式使函数可以获取对该向量的引用,而无需我们传递对它的引用。

如果你有一个局部变量,其地址从未被占用,编译器可以假定非内联函数调用不能改变它。 因为任何全局变量都无法保存指向此对象的指针。 (这称为逃逸分析(。 这就是为什么编译器可以在函数调用之间将size_t i保存在寄存器中的原因。 (int i可以得到优化,因为它被size_t i遮蔽了,而不是在其他方面使用(。

它可以对本地vector执行相同的操作(即用于基本、end_size和end_capacity指针(。

ISO C99对此问题有一个解决方案:int *restrict foo。 许多C++编译都支持int *__restrict foo,以保证foo指向的内存只能通过该指针访问。 在采用 2 个数组的函数中最常用,并且您希望向编译器承诺它们不会重叠。 因此,它可以自动矢量化,而无需生成代码来检查并运行回退循环。

OP评论说:

在 Rust 中,不可变引用是一个全局保证,即没有其他人正在改变你引用的值(相当于 C++restrict(

这就解释了为什么 Rust 可以进行这种优化,而C++不能。


优化您的C++

显然,您应该在函数的顶部使用一次auto size = input.size();,以便编译器知道它是一个循环不变量。 C++实现不能为您解决此问题,因此您必须自己完成。

您可能还需要const int *data = input.data();std::vector<int>"控制块"中提升数据指针的负载。不幸的是,优化可能需要非常不惯用的源更改。

Rust 是一种更现代的语言,是在编译器开发人员了解编译器在实践中的可能性之后设计的。 它确实也以其他方式显示,包括便携式暴露CPU可以通过i32.count_ones,旋转,位扫描等做的一些很酷的事情。 ISO C++仍然没有在便携式上公开任何这些,这真的很愚蠢,除了std::bitset::count().