SSE微优化指令顺序

10
我注意到有时候MSVC 2010根本不重新排序SSE指令。我认为在循环内部我不需要关心指令的顺序,因为编译器会处理最佳方案,但这似乎并非如此。
我该如何思考这个问题?是什么决定了最佳指令顺序?我知道一些指令比其他指令具有更高的延迟,并且某些指令可以在CPU级别上并行/异步运行。哪些指标在这种情况下是相关的?我在哪里可以找到它们?
我知道我可以通过性能分析避免这个问题,但这样的性能分析器很昂贵(VTune XE),而且我想知道背后的理论,而不仅仅是实证结果。
另外,我应该关心软件预取(_mm_prefetch),还是可以假设CPU会比我做得更好?
假设我有以下函数。我应该交错一些指令吗?我应该在流之前存储所有数据,按顺序加载所有数据,然后进行计算等吗?我需要考虑USWC vs非USWC,以及暂态vs非暂态吗?
            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);

1
我认为这个答案必须通过有限的测试来得出。虽然x86现在已经有了OOE,但无论顺序如何,它都可以处理这种情况并且接近最优。 - Flexo
1
@Skizz - 我知道,但在某种程度上,实际执行它的人并不重要。如果两个人都可以做到,但只有一个人在做,如果你花费精力强迫另一个人也去做,你不会注意到任何区别。 - Flexo
3
为什么你需要一个昂贵的分析器来做这件事?你可以通过手动计时来轻松完成。只需获取当前时间,运行代码数十亿次,然后再次获取时间,并除以迭代所需的时间来计算每次迭代所需的时间。 - jalf
10
你是否真正编写过SSE代码?它对顺序、缓存效应以及几乎任何其他因素都非常敏感���称其为浪费时间只是愚蠢的说法。通常,只有在关心性能时才会使用SSE指令集,此时高效执行指令根本不是“浪费时间”。 - jalf
1
是的,我已经写过SSE了。我并不是在暗示顺序无关紧要,我的意思是处理器具有最佳优化知识,因此如果它重新排序,那么当编译器不知道它将运行的确切芯片时,编译器如何超越该性能。芯片清楚地知道它是什么。 - Tavison
显示剩余6条评论
4个回答

9
我同意大家的看法,测试和调整是最好的方法。但是有一些技巧可以帮助你。
首先,MSVC确实会重新排列SSE指令。你的示例可能过于简单或已经达到最优状态。
一般来说,如果你有足够的寄存器,完全交错 tends tend to give the best results. 为了更进一步,展开循环以使用所有寄存器,但不要展开得太多,以免溢出。在你的示例中,循环完全受制于内存访问,所以没有太多改进的空间。
在大多数情况下,不必使指令的顺序完美无缺就能实现最佳性能。只要它足够“接近”,编译器或硬件的乱序执行都将为您解决问题。
我用来确定代码是否最优的方法是关键路径和瓶颈分析。写完循环后,我查找哪些指令使用哪些资源。利用这些信息,我可以计算出性能的上限,然后将其与实际结果进行比较,以查看离最优有多远。
例如,假设我有一个有100个加法和50个乘法的循环。在英特尔和AMD(Bulldozer之前)上,每个核心可以维持每个周期一个SSE/AVX加法和一个SSE/AVX乘法。由于我的循环有100个加法,所以我知道我不能比100个周期更好。是的,乘法器一半的时间会闲置,但加法器是瓶颈。
现在我去计时我的循环,我得到105个周期每次迭代。这意味着我已经非常接近最优状态,没有太多可获得的了。但如果我得到250个周期,那就意味着循环有问题,值得更多地调整它。
关键路径分析遵循同样的思路。查找所有指令的延迟,并找到循环的关键路径的周期时间。如果你的实际性能非常接近它,你已经是最优的了。
Agner Fog对当前处理器的内部细节有很好的参考资料:http://www.agner.org/optimize/microarchitecture.pdf

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可以同时预取最多四个流。


6

你可能会发现Intel架构优化参考手册的第5章到第7章非常有趣,它详细介绍了Intel认为你应该如何编写最佳SSE代码,并且详细说明了你所提出的许多问题。


1
我也想推荐Intel® Architecture Code Analyzer工具:

https://software.intel.com/en-us/articles/intel-architecture-code-analyzer

这是一个静态代码分析器,有助于找出/优化关键路径、延迟和吞吐量。它适用于Windows、Linux和MacOs(我只在Linux上尝试过)。文档中有一个中等简单的示例,说明如何使用它(即通过重新排序指令来避免延迟)。

它还不错,但已经不再维护了。最后支持的微架构是Haswell。在调整Skylake时仍然有用,但希望英特尔能再次更新它。它并不完美,有许多限制,并且偶尔其数字与真实硬件不一致,但它绝对是有用的。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接