【问题标题】:Visual Studio C++ compiler generates 3x slower code when changing completely unrelated code更改完全不相关的代码时,Visual Studio C++ 编译器生成慢 3 倍的代码
【发布时间】:2016-12-10 14:46:48
【问题描述】:

我有一个嵌套的 for 循环,它生成以下程序集:

# branch target labels manually added for readability
002E20F8  mov         ebx,esi  
002E20FA  mov         dword ptr [ebp-10h],3B9ACA00h  
002E2101  sub         ebx,edi  
002E2103  add         ebx,7  
002E2106  shr         ebx,3  
002E2109  nop         dword ptr [eax]  
  outer_loop:
002E2110  xor         eax,eax  
002E2112  xor         ecx,ecx  
002E2114  cmp         edi,esi  
002E2116  mov         edx,ebx  
002E2118  cmova       edx,eax  
002E211B  mov         eax,edi  
002E211D  test        edx,edx 
002E211F  je          main+107h (02E2137h)  ;end_innerloop

  inner_loop:           
002E2121  movsd       xmm0,mmword ptr [eax] 
002E2125  inc         ecx                     ; inc/addsd swapped
002E2126  addsd       xmm0,mmword ptr [k]   
002E212B  add         eax,8  
002E212E  movsd       mmword ptr [k],xmm0  
002E2133  cmp         ecx,edx  
002E2135  jne         main+0F1h (02E2121h)  ;inner_loop
  end_innerloop:        
002E2137  sub         dword ptr [ebp-10h],1  
002E213B  jne         main+0E0h (02E2110h)   ;outer_loop

如果我在嵌套的 for 循环之前更改一行代码以简单地声明一个 int,然后在 for 循环之后将其打印出来。这使得编译器将 k 的存储/重新加载拉出循环。

问题的第一个版本将此描述为“以稍微不同的顺序生成指令”。 (编者注:也许我应该留下这个分析/更正的答案?)

003520F8  mov         ebx,esi  
003520FA  mov         dword ptr [ebp-10h],3B9ACA00h  
00352101  sub         ebx,edi  
00352103  add         ebx,7  
00352106  shr         ebx,3  
00352109  nop         dword ptr [eax]  
  outer_loop:
00352110  xor         eax,eax  
00352112  xor         ecx,ecx  
00352114  cmp         edi,esi  
00352116  mov         edx,ebx  
00352118  cmova       edx,eax  
0035211B  mov         eax,edi  
0035211D  test        edx,edx  
0035211F  je          main+107h (0352137h) ;end_innerloop

00352121  movsd       xmm0,mmword ptr [k]    ; load of k hoisted out of the loop.  Strangely not optimized to xorpd xmm0,xmm0

  inner_loop:
00352126  addsd       xmm0,mmword ptr [eax]
0035212A  inc         ecx  
0035212B  add         eax,8  
0035212E  cmp         ecx,edx  
00352130  jne         main+0F6h (0352126h)  ;inner_loop

00352132  movsd       mmword ptr [k],xmm0     ; movsd in different place.

  end_innerloop:
00352137  sub         dword ptr [ebp-10h],1  
0035213B  jne         main+0E0h (0352110h)  ;outer_loop

编译器的第二种安排速度快了 3 倍。我对此感到有些震惊。有谁知道怎么回事?

这是使用 Visual Studio 2015 编译的。

编译器标志(如果需要,我可以添加更多):

优化:最大化速度/O2

代码:

#include <iostream>
#include <vector>
#include "Stopwatch.h"

static constexpr int N = 1000000000;

int main()
{
    std::vector<double> buffer;

    buffer.resize(10);

    for (auto& i : buffer)
    {
        i = 1e-100;
    }

    double k = 0;
    int h = 0; // removing this line and swapping the lines std::cout << "time = "... results in 3x slower code??!!

    Stopwatch watch;

    for (int i = 0; i < N; i++)
    {
        for (auto& j : buffer)
        {
            k += j;
        }
    }

    //std::cout << "time = " << watch.ElapsedMilliseconds() << " / " << k << std::endl;
    std::cout << "time = " << watch.ElapsedMilliseconds() << " / " << k << " / " << h << std::endl;

    std::cout << "Done...";
    std::getchar();

    return EXIT_SUCCESS;
}

秒表类:

#pragma once

#include <chrono>

class Stopwatch
{
private:
    typedef std::chrono::high_resolution_clock clock;
    typedef std::chrono::microseconds microseconds;
    typedef std::chrono::milliseconds milliseconds;

    clock::time_point _start;

public:
    Stopwatch()
    {
        Restart();
    }

    void Restart()
    {
        _start = clock::now();
    }

    double ElapsedMilliseconds()
    {
        return ElapsedMicroseconds() * 1E-3;
    }

    double ElapsedSeconds()
    {
        return ElapsedMicroseconds() * 1E-6;
    }

    Stopwatch(const Stopwatch&) = delete;
    Stopwatch& operator=(const Stopwatch&) = delete;

private:
    double ElapsedMicroseconds()
    {
        return static_cast<double>(std::chrono::duration_cast<microseconds>(clock::now() - _start).count());
    }
};

【问题讨论】:

  • 你为什么不发布代码?!另外,对于我们这些普通人,您会强调区别吗?我的眼睛在流血……
  • 另外您使用了什么优化级别以及您使用的是什么版本的 MSVS?
  • 对于 x64 Visual Studio 2015 Release update 2,我的 skylake i7 笔记本电脑 time = 13815.8 / 1e-90 / 0time = 13824.5 / 1e-90 的时间几乎相同。我第二次尝试这个(这次不是电池)时间稍微快一点:time = 13284.6 / 1e-90 / 0 vs time = 13297 / 1e-90
  • 唯一重要的是内部循环外部的mmword ptr [k],xmm0,我认为这是加速的原因(对k的内存位置的写入减少了10倍)。该指令甚至可以在两个循环之外进行优化,从而获得更大的加速。
  • 我有点想知道为什么优化器不只运行一次数组 sum,然后 *N.... 或至少 Nx +sum。在溢出的情况下可能会破坏一些准确性规则或某些东西,所以结果会不会与慢慢添加它相同?我不明白,它看起来像是优化器跳入的完美简单示例,并猜测外部和/或内部循环的无用性。我最近刚刚看到了非常聪明的第一步展开(使用 gcc)的案例,现在这很奇怪。我会坚持我的手部优化:)(高级,算法,我的意思是,像k = N * std::accumulate(...);

标签: c++ performance visual-c++ assembly x86


【解决方案1】:

在编辑问题以修复令人困惑的换行符并在jcc 指令中的地址前添加分支目标标签以弄清楚代码实际在做什么之后,很明显循环显着不同。 movsd 不会在循环内重新排序;它在循环之外

我没有将这些内容留在问题中并在答案中进行更正,而是决定编辑问题并在此处讨论。我认为代码块足够长,以至于未来的读者会因为试图跟踪 4 个版本的代码而陷入困境,而且它并不能帮助有相同问题的人通过搜索引擎找到它。


快速版本将k 保存在寄存器中(xmm0),而慢速版本在每次迭代时重新加载/存储它。这通常表明编译器的别名分析未能证明事物不能重叠。

伤害的不是额外的存储和加载本身,而是它通过存储转发延迟延长循环携带的依赖链从一次迭代中的存储到加载下一次迭代。存储转发延迟在现代 Intel CPU 上大约为 6 个周期,而 addsd 为 3 个周期(例如在 Haswell 上)。所以这完美地解释了 3 加速的因素:

  • 当循环携带的依赖链为addsd + store-forwarding时,每次迭代9个循环
  • 当循环携带的依赖链只是addsd时,每次迭代3个循环

有关指令表和微架构详细信息,请参阅 http://agner.org/optimize/。还有标签wiki中的其他链接。


IDK MSVC 如何无法证明k 不与任何内容重叠,因为它是一个本地地址,其地址不会转义函数。 (它的地址甚至没有被占用)。 MSVC 在那里做得很糟糕。它也应该只是xorps xmm0,xmm0 在循环之前将其归零,而不是加载一些归零的内存。我什至看不到它在哪里将任何内存归零。我想这不是整个函数的汇编。

如果您使用 MSVC 的 -ffast-math 等价物进行编译,它可能会向量化缩减(使用 addpd),并希望有多个累加器。尽管使用这样一个 tiny 向量,您循环了很多次,但非 4 的倍数元素计数有点不方便。尽管如此,循环开销在这里不是问题。即使k 保存在寄存器中,循环携带的依赖链也占主导地位,因为您的代码只使用一个累加器。每 3 个时钟一个 addsd 为其他 insn 运行留下大量时间。

理想情况下,允许关联 FP 数学重新排序将使编译器像 @Ped7g 建议的那样将其优化为 k = N * std::accumulate(...);,将数组上的总和视为公共子表达式。


顺便说一句,初始化向量有很多更好的方法:

不要调整向量的大小(使用默认构造函数构造新元素)并然后写入新值,你应该只做类似

的事情
std::vector<double> buffer(10, 1e-100);   // 10 elements set to 1e-100

这样可以确保 asm 在存储所需值之前不会浪费时间存储零。我认为resize 也可以将值复制到新元素中,因此您仍然可以声明一个空向量然后调整它的大小。

【讨论】:

  • 感谢您抽出宝贵时间提供一些非常有用的信息。
猜你喜欢
  • 2012-07-25
  • 2021-06-17
  • 1970-01-01
  • 2010-11-14
  • 1970-01-01
  • 2010-09-06
  • 1970-01-01
  • 2019-09-25
  • 1970-01-01
相关资源
最近更新 更多