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