为什么 GCC 不能假设 std::vector::size 在这个循环中不会改变?
Why can't GCC assume that std::vector::size won't change in this loop?
我向一位同事声称,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
我检查了一下,我很确定r12
是xs.len() - 1
,rbx
是计数器。早些时候有一个用于rbx
的add
和一个循环外进入r12
的mov
。
这是为什么呢?似乎如果 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()
.
- 为什么 GCC 不能假设 std::vector::size 在这个循环中不会改变?
- 为什么切换 for 循环的顺序会显著改变执行时间?
- 为什么使用 2 个嵌套循环 O(n^2) 复杂度来解决二和问题,当只改变循环计数器逻辑时运行得更快?
- 我的c ++数组值不想改变,它输出无限循环
- 如何在循环中改变容器中的元素
- 为什么我的矢量中的项目在 C++ 中的 for-each 循环期间不会改变?
- pi的倍数到千分之一的值可能会改变循环执行方式
- 变量的范围大于for循环,为什么它不改变值
- 我在改变循环方面并没有错
- 如何在for循环中改变char*中的字符
- 为什么我的循环范围不会改变元素?
- 如何改变for循环,使我可以使用eof
- 改变字符串的循环总是失败
- 改变while循环以适应两种情况
- 改变相邻顶点的值并移除自循环
- 在c++中循环内的值不会改变
- for循环中的奇怪行为改变了结果
- 我怎么能使循环和改变number为字符串
- Windows 8 APP,用数据绑定改变XAML文本框的文本.游戏循环工作线程导致问题
- 在c++中,循环后无法改变整数的值