C++ 为什么这两个循环在使用-O3编译时运行速度相同,而在使用-O2编译时运行速度不同?
在下面的程序中,由于依赖指令,我希望test1运行得较慢。用-O2进行的测试似乎证实了这一点。但是后来我试着用-O3,现在时间差不多相等了。这怎么可能C++ 为什么这两个循环在使用-O3编译时运行速度相同,而在使用-O2编译时运行速度不同?,c++,optimization,C++,Optimization,在下面的程序中,由于依赖指令,我希望test1运行得较慢。用-O2进行的测试似乎证实了这一点。但是后来我试着用-O3,现在时间差不多相等了。这怎么可能 #include <iostream> #include <vector> #include <cstring> #include <chrono> volatile int x = 0; // used for preventing certain optimizations enum {
#include <iostream>
#include <vector>
#include <cstring>
#include <chrono>
volatile int x = 0; // used for preventing certain optimizations
enum { size = 60 * 1000 * 1000 };
std::vector<unsigned> a(size + x); // `size + x` makes the vector size unknown by compiler
std::vector<unsigned> b(size + x);
void test1()
{
for (auto i = 1u; i != size; ++i)
{
a[i] = a[i] + a[i-1]; // data dependency hinders pipelining(?)
}
}
void test2()
{
for (auto i = 0u; i != size; ++i)
{
a[i] = a[i] + b[i]; // no data dependencies
}
}
template<typename F>
int64_t benchmark(F&& f)
{
auto start_time = std::chrono::high_resolution_clock::now();
f();
auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start_time);
return elapsed_ms.count();
}
int main(int argc, char**)
{
// make sure the optimizer cannot make any assumptions
// about the contents of the vectors:
for (auto& el : a) el = x;
for (auto& el : b) el = x;
test1(); // warmup
std::cout << "test1: " << benchmark(&test1) << '\n';
test2(); // warmup
std::cout << "\ntest2: " << benchmark(&test2) << '\n';
return a[x] * x; // prevent optimization and exit with code 0
}
优化器是非常复杂的软件,并不总是可预测的 使用g++5.2.0和-O2
test1
和test2
编译成类似的机器代码:
;;;; test1 inner loop
400c28: 8b 50 fc mov -0x4(%rax),%edx
400c2b: 01 10 add %edx,(%rax)
400c2d: 48 83 c0 04 add $0x4,%rax
400c31: 48 39 c1 cmp %rax,%rcx
400c34: 75 f2 jne 400c28 <_Z5test1v+0x18>
;;;; test2 inner loop
400c50: 8b 0c 06 mov (%rsi,%rax,1),%ecx
400c53: 01 0c 02 add %ecx,(%rdx,%rax,1)
400c56: 48 83 c0 04 add $0x4,%rax
400c5a: 48 3d 00 1c 4e 0e cmp $0xe4e1c00,%rax
400c60: 75 ee jne 400c50 <_Z5test2v+0x10>
而test2
使用xmm
注册并生成完全不同的机器代码,爆炸成一个看起来像是展开的版本。内部循环变为
;;;; test2 inner loop (after a lot of preprocessing)
400e30: f3 41 0f 6f 04 00 movdqu (%r8,%rax,1),%xmm0
400e36: 83 c1 01 add $0x1,%ecx
400e39: 66 0f fe 04 07 paddd (%rdi,%rax,1),%xmm0
400e3e: 0f 29 04 07 movaps %xmm0,(%rdi,%rax,1)
400e42: 48 83 c0 10 add $0x10,%rax
400e46: 44 39 c9 cmp %r9d,%ecx
400e49: 72 e5 jb 400e30 <_Z5test2v+0x90>
;;;;test2内部循环(经过大量预处理)
400e30:f3 41 0f 6f 04 00移动区(%r8,%rax,1),%xmm0
400e36:83 c1 01添加$0x1,%ecx
400e39:66 0f fe 04 07 paddd(%rdi,%rax,1),%xmm0
400e3e:0f 29 04 07 movaps%xmm0,(%rdi,%rax,1)
400e42:48 83 c0 10添加$0x10,%rax
400e46:44 39 c9 cmp%r9d%ecx
400e49:72 e5 jb 400e30
并为每个迭代执行多个添加
如果你想测试特定的处理器行为,可能直接在汇编程序中编写是一个更好的主意,因为C++编译器可能会对你原来的源代码做的重写。
< P>因为在<代码> -O3中,GCC通过存储<代码> A[i]的值有效地消除了数据依赖性。在寄存器中,并在下一次迭代中重用它,而不是加载a[i-1]
结果大致相当于:
void test1()
{
auto x = a[0];
auto end = a.begin() + size;
for (auto it = next(a.begin()); it != end; ++it)
{
auto y = *it; // Load
x = y + x;
*it = x; // Store
}
}
在-O2
中编译的代码生成与在-O3
中编译的代码完全相同的程序集
问题中的第二个循环在-O3
中展开,因此加速。应用的两种优化似乎与我无关,第一种情况更快是因为gcc删除了加载指令,第二种情况是因为它被展开
在这两种情况下,我不认为优化器做了任何特别的事情来改进缓存行为,这两种内存访问模式都很容易被cpu预测。提示:“可能直接在汇编程序中写入”您肯定是指汇编,因为汇编程序是将汇编源代码编译成目标代码的工具。@black:我从不喜欢“汇编”形式,我更喜欢使用“汇编语言”。显然,甚至维基百科也同意这是一个可行的选择……我看到了寄存器分配如何通过减少内存访问来改善这种情况。但似乎仍然存在一种依赖关系:更新寄存器必须在写入内存之前进行。这真的是更好的“Pipelineable”吗?@StackedCrooked是的,但写入可以在下一次迭代加载后重新排序(x86可以在加载后移动存储)。所以我认为cpu会加载一条缓存线,进行计算,然后更新缓存线。即使循环是流水线的,但由于指令太少,获取数据仍然是瓶颈,因此第二个循环的速度会变慢。
;;;; test2 inner loop (after a lot of preprocessing)
400e30: f3 41 0f 6f 04 00 movdqu (%r8,%rax,1),%xmm0
400e36: 83 c1 01 add $0x1,%ecx
400e39: 66 0f fe 04 07 paddd (%rdi,%rax,1),%xmm0
400e3e: 0f 29 04 07 movaps %xmm0,(%rdi,%rax,1)
400e42: 48 83 c0 10 add $0x10,%rax
400e46: 44 39 c9 cmp %r9d,%ecx
400e49: 72 e5 jb 400e30 <_Z5test2v+0x90>
void test1()
{
auto x = a[0];
auto end = a.begin() + size;
for (auto it = next(a.begin()); it != end; ++it)
{
auto y = *it; // Load
x = y + x;
*it = x; // Store
}
}