我注意到有时MSVC 2010根本没有重新排序SSE指令。我认为我不需要关心循环中的指令顺序,因为编译器处理得最好,但似乎并非如此。
我该如何看待这个?什么决定最佳指令顺序?我知道某些指令具有比其他指令更高的延迟,并且某些指令可以在cpu级别上并行/异步运行。哪些指标与上下文相关?我在哪里可以找到它们?
我知道我可以通过分析来避免这个问题,但是这些分析器很昂贵(VTune XE)和我想知道它背后的理论,而不仅仅是经验结果。
我是否应该关心软件预取(_mm_prefetch
)还是我可以假设cpu会比我做得更好?
假设我有以下功能。我应该交错一些指令吗?我应该在流之前做商店,按顺序完成所有负载然后进行计算等......?我是否需要考虑USWC与非USWC,以及时间与非时间?
auto cur128 = reinterpret_cast<__m128i*>(cur);
auto prev128 = reinterpret_cast<const __m128i*>(prev);
auto dest128 = reinterpret_cast<__m128i*>(dest;
auto end = cur128 + count/16;
while(cur128 != end)
{
auto xmm0 = _mm_add_epi8(_mm_load_si128(cur128+0), _mm_load_si128(prev128+0));
auto xmm1 = _mm_add_epi8(_mm_load_si128(cur128+1), _mm_load_si128(prev128+1));
auto xmm2 = _mm_add_epi8(_mm_load_si128(cur128+2), _mm_load_si128(prev128+2));
auto xmm3 = _mm_add_epi8(_mm_load_si128(cur128+3), _mm_load_si128(prev128+3));
// dest128 is USWC memory
_mm_stream_si128(dest128+0, xmm0);
_mm_stream_si128(dest128+1, xmm1);
_mm_stream_si128(dest128+2, xmm2);;
_mm_stream_si128(dest128+3, xmm3);
// cur128 is temporal, and will be used next time, which is why I choose store over stream
_mm_store_si128 (cur128+0, xmm0);
_mm_store_si128 (cur128+1, xmm1);
_mm_store_si128 (cur128+2, xmm2);
_mm_store_si128 (cur128+3, xmm3);
cur128 += 4;
dest128 += 4;
prev128 += 4;
}
std::swap(cur, prev);
答案 0 :(得分:9)
我同意每个人的看法,测试和调整是最好的方法。但是有一些技巧可以帮助它。
首先,MSVC 重新排序SSE指令。您的示例可能过于简单或已经非常优化。
一般来说,如果您有足够的寄存器来执行此操作,则完全交错可能会产生最佳结果。为了更进一步,展开你的循环足以使用所有寄存器,但不要太多溢出。 在您的示例中,循环完全受到内存访问的约束,因此没有太多空间可以做得更好。
在大多数情况下,没有必要使指令的顺序完美,以实现最佳性能。只要它“足够接近”,编译器或硬件的无序执行就会为你解决它。
我用来确定我的代码是否最优的方法是关键路径和瓶颈分析。在我编写循环之后,我查找哪些指令使用哪些资源。使用这些信息,我可以计算出性能的上限,然后我将其与实际结果进行比较,看看我与最优的距离是多远。
例如,假设我有一个包含100个加法和50个乘法的循环。在Intel和AMD(Bulldozer之前)上,每个核心可以支持每个周期一个SSE / AVX加法和一个SSE / AVX加法。 由于我的循环有100个添加,我知道我不能做任何超过100个循环。是的,乘数将在一半的时间内闲置,但加法器是瓶颈。
现在我去循环计时,每次迭代我得到105个循环。这意味着我非常接近最优,并且没有太多可以获得。但如果我得到250个循环,那么这就意味着循环出了问题,值得更多地修补它。
关键路径分析遵循相同的想法。查找所有指令的延迟,并找到循环关键路径的循环时间。如果您的实际表现非常接近,那么您已经达到最佳状态。
Agner Fog对当前处理器的内部细节有很好的参考: http://www.agner.org/optimize/microarchitecture.pdf
答案 1 :(得分:6)
我刚刚使用VS2010 32位编译器构建它,我得到以下内容:
void F (void *cur, const void *prev, void *dest, int count)
{
00901000 push ebp
00901001 mov ebp,esp
00901003 and esp,0FFFFFFF8h
__m128i *cur128 = reinterpret_cast<__m128i*>(cur);
00901006 mov eax,220h
0090100B jmp F+10h (901010h)
0090100D lea ecx,[ecx]
const __m128i *prev128 = reinterpret_cast<const __m128i*>(prev);
__m128i *dest128 = reinterpret_cast<__m128i*>(dest);
__m128i *end = cur128 + count/16;
while(cur128 != end)
{
auto xmm0 = _mm_add_epi8(_mm_load_si128(cur128+0), _mm_load_si128(prev128+0));
00901010 movdqa xmm0,xmmword ptr [eax-220h]
auto xmm1 = _mm_add_epi8(_mm_load_si128(cur128+1), _mm_load_si128(prev128+1));
00901018 movdqa xmm1,xmmword ptr [eax-210h]
auto xmm2 = _mm_add_epi8(_mm_load_si128(cur128+2), _mm_load_si128(prev128+2));
00901020 movdqa xmm2,xmmword ptr [eax-200h]
auto xmm3 = _mm_add_epi8(_mm_load_si128(cur128+3), _mm_load_si128(prev128+3));
00901028 movdqa xmm3,xmmword ptr [eax-1F0h]
00901030 paddb xmm0,xmmword ptr [eax-120h]
00901038 paddb xmm1,xmmword ptr [eax-110h]
00901040 paddb xmm2,xmmword ptr [eax-100h]
00901048 paddb xmm3,xmmword ptr [eax-0F0h]
// dest128 is USWC memory
_mm_stream_si128(dest128+0, xmm0);
00901050 movntdq xmmword ptr [eax-20h],xmm0
_mm_stream_si128(dest128+1, xmm1);
00901055 movntdq xmmword ptr [eax-10h],xmm1
_mm_stream_si128(dest128+2, xmm2);;
0090105A movntdq xmmword ptr [eax],xmm2
_mm_stream_si128(dest128+3, xmm3);
0090105E movntdq xmmword ptr [eax+10h],xmm3
// cur128 is temporal, and will be used next time, which is why I choose store over stream
_mm_store_si128 (cur128+0, xmm0);
00901063 movdqa xmmword ptr [eax-220h],xmm0
_mm_store_si128 (cur128+1, xmm1);
0090106B movdqa xmmword ptr [eax-210h],xmm1
_mm_store_si128 (cur128+2, xmm2);
00901073 movdqa xmmword ptr [eax-200h],xmm2
_mm_store_si128 (cur128+3, xmm3);
0090107B movdqa xmmword ptr [eax-1F0h],xmm3
cur128 += 4;
00901083 add eax,40h
00901086 lea ecx,[eax-220h]
0090108C cmp ecx,10h
0090108F jne F+10h (901010h)
dest128 += 4;
prev128 += 4;
}
}
表示编译器按照“写入寄存器后不立即使用寄存器”的一般规则重新排序指令。它还将两个加载和一个加载转换为单个加载和内存添加。您没有理由不能自己编写这样的代码并使用所有SIMD寄存器而不是您当前使用的四个寄存器。您可能希望将加载的总字节数与缓存行的大小相匹配。这将使硬件预取有机会在您需要之前填充下一个缓存行。
此外,预取,特别是在代码中顺序读取内存,通常不是必需的。 MMU一次最多可以预取四个流。
答案 2 :(得分:6)
您可能会发现Intel Architectures Optimization Reference Manual的第5章到第7章非常有趣,它详细说明了英特尔认为您应该如何编写最佳SSE代码,并详细介绍了您要问的很多问题。
答案 3 :(得分:1)
我还想推荐英特尔®架构代码分析器:
https://software.intel.com/en-us/articles/intel-architecture-code-analyzer
它是一个静态代码分析器,可帮助确定/优化关键路径,延迟和吞吐量。它适用于Windows,Linux和MacO(我只在Linux上试过)。该文档有一个关于如何使用它的中等简单示例(即,如何通过重新排序指令来避免延迟)。