2011-09-01 50 views
10

我注意到有时MSVC 2010根本没有重新排序SSE指令。因为编译器处理的最好,我认为我不必关心循环内部的指令顺序,这似乎并不是这种情况。SSE微优化指令订单

我应该怎么想?什么决定最佳指令顺序?我知道一些指令比其他指令具有更高的延迟,并且某些指令可以在CPU级别上并行/异步运行。在上下文中哪些指标是相关的?我可以在哪里找到它们?

我知道我可以避免通过分析这个问题,但是这样的廓线仪价格昂贵(VTune™可视化XE)和我想知道它背后的理论,而不仅仅是emperical结果。

另外我应该关心软件预取(_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); 
+1

我认为这个问题的答案必须是在测量试验。尽管x86已经有[OOE](http://en.wikipedia.org/wiki/Out-of-order_execution)很长一段时间了,但无论排序如何,它都可以很好地处理这种情况。 – Flexo

+0

测试总是最好的。但是在这种情况下,它需要一个相当昂贵的分析器,例如, VTune XE。我想更多地了解它背后的理论,而不是实证结果。 OOE走多远?这是内存延迟还是指令延迟?如果重新订购,OOE是否照顾可以并行运行的指令? – ronag

+0

你可以发布这个发布版本的汇编程序输出吗?看看编译器用这个做什么会很有趣。 – Skizz

回答

9

我同意每个人都说测试和调整是最好的方法。但是有一些技巧可以帮助它。

首先,MSVC 确实重新排序SSE指令。你的例子可能太简单或已经是最优的。

一般来说,如果你有足够的寄存器这样做,完全交错往往会给出最好的结果。更进一步,请展开足够的循环以使用所有寄存器,但不要太多以致漏出。 在你的例子中,循环完全受到内存访问的限制,所以没有太多空间可以做得更好。

在大多数情况下,没有必要获得完美的指令顺序以实现最佳性能。只要“足够接近”,编译器或硬件的乱序执行都可以为您解决问题。

我用它来确定,如果我的代码是最佳的方法是关键路径和瓶颈分析。在我编写循环之后,我查找了哪些指令使用哪些资源。使用这些信息,我可以计算性能的上限,然后将其与实际结果进行比较,以查看我与最优的距离有多远。

例如,假设我有100将与50个相乘的环路。在英特尔和AMD(推土机推土机)上,每个核心可以在每个周期支持一个SSE/AVX添加和一个SSE/AVX乘法。 由于我的循环有100个增加,我知道我不能做任何比100个周期更好的。是的,乘数在一半时间内都会闲置,但加法器是瓶颈。

现在我去和我的时间循环,我得到每个迭代周期105。这意味着我非常接近最佳状态,并且没有太多的收获。但是,如果我获得了250个周期,那么这意味着循环出现问题,值得更多修补。

关键路径分析遵循同样的想法。查找所有指令的延迟时间并查找循环关键路径的周期时间。如果你的实际表现非常接近它,那么你已经是最佳了。

昂纳雾对当前处理器的内部细节有很大的参考: 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一次最多可以预取四个数据流。

1

我也想推荐的英特尔®架构代码分析器:

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

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

+0

这很好,但不再维护。最后一个支持的微体系结构是Haswell。调优Skylake时,这仍然很有用,但希望英特尔会再次开始更新。这并不完美,有很多限制,偶尔它的数字不符合真正的硬件,但它绝对有用。 –