【发布时间】: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 / 0与time = 13824.5 / 1e-90的时间几乎相同。我第二次尝试这个(这次不是电池)时间稍微快一点:time = 13284.6 / 1e-90 / 0vstime = 13297 / 1e-90 -
唯一重要的是内部循环外部的
mmword ptr [k],xmm0,我认为这是加速的原因(对k的内存位置的写入减少了10倍)。该指令甚至可以在两个循环之外进行优化,从而获得更大的加速。 -
我有点想知道为什么优化器不只运行一次数组 sum,然后 *N.... 或至少 Nx +sum。在溢出的情况下可能会破坏一些准确性规则或某些东西,所以结果会不会与慢慢添加它相同?我不明白,它看起来像是优化器跳入的完美简单示例,并猜测外部和/或内部循环的无用性。我最近刚刚看到了非常聪明的第一步展开(使用 gcc)的案例,现在这很奇怪。我会坚持我的手部优化:)(高级,算法,我的意思是,像
k = N * std::accumulate(...);)
标签: c++ performance visual-c++ assembly x86