更复杂的循环可以执行得更快吗?
Can more complex loop be executed faster?
我在准备用c++实现类似python的",".join(vector)
函数时发现了这一点。我想比较一下,为了避免在字符串的开头放置额外的','
,消除内部if first element
条件是否有意义。我写了以下函数:
long long test1() // 38332ms
{
long long k = 0;
for (long long i = 0; i < 10000000000; ++i)
{
if (i != 0)
{
k += 2;
}
k += 1;
}
return k;
}
long long test2() // 45272ms
{
long long k = 0;
k += 1;
for (long long i = 1; i < 10000000000; ++i)
{
k += 2; // in the real code it might be impossible to merge
k += 1; // those operations
}
return k;
}
我使用简化的代码使条件跳转更有意义。我期望分支预测能够最小化差异。令我惊讶的是,第一个函数表现得更好。我正在测试VS2015下的调试设置。编译器没有执行任何优化—我检查了程序集。我也试着移动一些东西——移动函数定义,调用顺序,极限常数。比例大致相同。
可能的解释是什么?
编辑:而不是分析这个特定的场景,我试图得到一些关于这种行为的可能原因的一般性答案。我的猜测是,CPU在分支预测期间执行了某种启发式操作,在我的特定情况下,我的特定CPU在test1中更好地预测了这个分支。这只是我的直觉,所以我在想它是否正确。有人能考虑一下我的猜测吗?
因为您是在调试模式下编译的,所以您的循环可能会在每次迭代中将k
存储/重新加载到内存中(循环计数器也是如此)。未优化代码中的微小循环通常是存储转发延迟的瓶颈(在Intel Haswell上约为5个周期),而不是任何类型的吞吐量瓶颈。
我的猜测是CPU在分支预测期间执行某种启发式,并且在我的特定情况下,我的特定CPU在test1中更好地预测该分支。
这没有意义。完美的分支预测可以将分支的成本降低到零,而不是使其为负。
。执行两个ADD指令+一个分支不会比只执行两个ADD指令快,除非还有其他不同的地方。
假设调试模式下的MSVC最终为test1编写的代码比为test2编写的代码少。也许它将k
保存在一个增量寄存器中,而不是使用内存目的地进行两个增量?
如果你发布了asm,它可能会告诉你编译器做了什么不同,使第一个版本运行得更快,但它不会有用或有趣。(与分支预测无关;它应该至少有99.999%的预测成功率,所以实际的分支指令几乎是自由的。)
如果你想了解cpu的内部工作原理,以及如何预测汇编指令循环的运行速度,请阅读Agner Fog的microarch pdf。
参见我对另一个关于优化调试模式的问题的回答,为什么它毫无意义,浪费时间,对于学习什么是快的,什么是慢的没有用处。
优化不只是通过一个常数来加速一切,它改变了你将遇到的瓶颈类型。(例如,k
将存在于寄存器中,因此k+=1
只有ADD指令的延迟,而不是通过内存往返的延迟)。当然,合并成k+=3
,并证明i!=0
分支只发生一次,并剥离该迭代。
更重要的是,结果是一个编译时常数,因此理想情况下,两个函数将编译成一条指令,并将结果放入返回值寄存器中。不幸的是,gcc6.2、clang3.9和icc17在版本1中都没有做到这一点,所以看起来手动剥离第一次迭代非常有帮助。
但是对于test2,所有的编译器都可以很容易地将其编译为movabs rax, 29999999998
/ret
。
相关:优化编译器如何使用这些函数
如果你想查看编译器的输出,使用函数参数而不是常量。您已经返回了结果,所以这很好。
我把你的函数放在Godbolt编译器资源管理器上(它最近添加了一个比icc13更新的英特尔编译器版本)。
clang-3.9愚蠢地使用cmov
作为test1,从- 01到-O3。一根可预测的树枝最好是跳下去,如果它没有完全剥落。(也许一个无符号循环计数器/上界可以帮助编译器弄清楚发生了什么,但我没有尝试。)
gcc为test1使用了一个条件分支,为test2使用了一个函数参数计数器版本。分支看起来很合理,有i!=0病例为CMP/乙脑的未采取方。但实际上它仍然会将循环计数器计数到最大值,并增加k
。
在test2中,gcc只是运行循环计数器并在循环外将其相乘。
ICC17非常积极地展开test1,使用多个整数寄存器作为累加器,因此可以并行执行多个ADD或INC。IDK,如果它有帮助的话,因为它溢出了它的一个累加器,内存目的地ADD。实际上,我认为它的展开足以使~20上循环每次迭代只被内存目的地ADD(在Intel Haswell上)的瓶颈减慢5到6个周期,而不是整数ALU ADD/INC指令的瓶颈(在Haswell上每个时钟4个)。它在不同的寄存器中执行+=2和+=1,并在末尾组合(我假设,我只是用cmp rdx, 1249999999
结束条件查看了循环。有趣的是,它没有像test2那样将循环优化到一个常量,而是积极地优化循环。
- 在执行其他功能的同时播放动画(LED矩阵和Arduino/ESP8266)
- C++,系统无法执行指定的程序
- 使用C++中的模板和运算符重载执行矩阵运算
- 创建一个函数以在输入为负数或零时输出字符串.第一次执行用户定义的函数
- 函数复杂度分析
- 执行函数时导致崩溃的变量
- 向量 <int> a {N, 0} 和 int arr a[N] = {0} 的时间复杂度有什么区别
- while循环中while循环的时间复杂度是多少
- 无论条件是否为true,if总是在c++中执行
- 当函数模板参数是具有默认参数的类模板时,函数模板参数的推导如何执行
- 在C++中对T*类型执行std::move的意外行为
- 使用QProcess执行命令,并将结果存储在QStringList中
- 如何在没有信号的情况下从C++执行QML插槽
- 如何确认我的constexpr表达式实际上已经在编译时执行
- VIM:执行复杂文件类型的脚本和显示结果
- C++ 模板复杂图像读取类执行时间慢,声明和实现分离
- Arduino UNO在执行更复杂的程序时意外循环Serial
- 更复杂的循环可以执行得更快吗?
- 如何在复杂情况下执行宏替换
- 如何在Qt中执行复杂的linux命令