Warning: file_get_contents(/data/phpspider/zhask/data//catemap/1/vue.js/6.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C++ 为什么这两个循环在使用-O3编译时运行速度相同,而在使用-O2编译时运行速度不同?_C++_Optimization - Fatal编程技术网

C++ 为什么这两个循环在使用-O3编译时运行速度相同,而在使用-O2编译时运行速度不同?

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 {

在下面的程序中,由于依赖指令,我希望test1运行得较慢。用-O2进行的测试似乎证实了这一点。但是后来我试着用-O3,现在时间差不多相等了。这怎么可能

#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
    }
}