我有一个嵌套的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());
}
};
答案 0 :(得分:5)
编辑问题以修复容易混淆的换行符,并在jcc
说明中的地址前面添加分支目标标签,以弄清楚代码实际在做什么,很明显循环显着不同。 movsd
不会在循环内重新排序;它在外面循环。
我决定编辑问题并在此处讨论,而不是将问题留在问题中并在答案中进行纠正。我认为代码块足够长,以至于未来的读者只会陷入困境,试图跟踪代码的4个版本,并且它不会帮助有同样问题的人用搜索引擎找到它。 / p>
快速版本将k
保留在寄存器(xmm0
)中,而慢速版本在每次迭代时重新加载/存储它。这通常表明编译器的别名分析无法证明事情不会重叠。
这不是额外的存储和负载本身受到伤害,而是它通过存储转发延迟延长了存储转发延迟从一次迭代中的商店到负载中的负载。下一次迭代。现代英特尔CPU上的存储转发延迟类似于6个周期,而addsd
则为3个周期(例如Haswell)。这样就完美地解释了3加速的因素:
addsd
+ store-forwarding addsd
有关说明表和微观详细信息,请参阅http://agner.org/optimize/。还有x86标记wiki中的其他链接。
IDK MSVC如何无法证明k
与任何内容不重叠,因为它是一个本地,其地址不会逃脱该功能。 (甚至没有提到它的地址)。 MSVC在那里做得很糟糕。它也应该只是xorps xmm0,xmm0
在循环之前将它归零,而不是加载一些归零的内存。我甚至没有看到它将任何记忆归零;我猜这不是整个功能的主题。
如果您使用MSVC等效的-ffast-math
进行编译,则可以对减少进行向量化(使用addpd
),并希望有多个累加器。虽然使用这样的 tiny 向量可以循环多次,但非多重4元素计数会有一些不方便。但是,循环开销在这里不是问题;即使k
保存在寄存器中,循环携带的依赖链也占主导地位,因为您的代码只使用一个累加器。每3个时钟一个addsd
为其他insn运行留下了大量时间。
理想情况下,允许关联FP数学重新排序会使编译器将其优化为k = N * std::accumulate(...);
,如@ Ped7g建议的那样,将数组上的和作为公共子表达式处理。
不是调整矢量大小(使用默认构造函数构造新元素)而然后编写新值,您应该执行类似
的操作std::vector<double> buffer(10, 1e-100); // 10 elements set to 1e-100
这确保了asm在存储您想要的值之前不会浪费时间存储零。我认为resize
也可以将值复制到新元素中,因此您仍然可以声明一个空向量然后调整大小。