【问题标题】:SSE micro-optimization instruction orderSSE微优化指令顺序
【发布时间】:2011-11-08 06:54:48
【问题描述】:

我注意到有时 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);

【问题讨论】:

  • 我认为这个问题的答案必须在测量测试中。尽管 x86 已经有 OOE 很长一段时间了,但无论排序如何,它都可以很好地处理这种情况。
  • @Skizz - 我知道,但在某种程度上,这并没有太大的区别实际上是在做这件事。如果是两个都可以做到而只有一个在做的情况下,如果你努力强迫另一个人也这样做,那么你根本不会注意到任何区别。跨度>
  • 为什么你需要一个昂贵的分析器呢?你可以很容易地手动计时。只需获取当前时间,将代码运行数十亿次,然后再次获取时间,然后除以得到每次迭代所花费的时间。
  • 我同意@awoodland。重新排序指令不仅浪费时间,而且您不知道其他机器上正在使用什么指令集。什么版本的 SSE 会有所作为。您可以尝试以几种方式改组说明,看看是否有任何变化。我对此表示怀疑。
  • @Tavison:真的编写过 SSE 代码吗?它对排序、缓存效果和其他任何东西都非常敏感。说这是浪费时间是愚蠢的。您通常只在关心性能时才使用 SSE,然后高效执行指令并不是“浪费时间”。

标签: c++ optimization sse simd micro-optimization


【解决方案1】:

我刚刚使用 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】:

    您可能会发现Intel Architectures Optimization Reference Manual 的第 5 章到第 7 章非常有趣,它详细说明了英特尔认为您应该如何编写最佳 SSE 代码,并且详细说明了您提出的许多问题。

    【讨论】:

      【解决方案3】:

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

      首先,MSVC 重新排序 SSE 指令。您的示例可能过于简单或已经最优。

      一般来说,如果您有足够的寄存器来执行此操作,则完全交错往往会产生最佳结果。更进一步,展开循环以使用所有寄存器,但不要过多溢出。 在您的示例中,循环完全受内存访问限制,因此没有太多空间可以做得更好。

      在大多数情况下,没有必要让指令的顺序完美以达到最佳性能。只要它“足够接近”,编译器或硬件的乱序执行都会为您修复它。

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

      例如,假设我有一个包含 100 次加法和 50 次乘法的循环。在 Intel 和 AMD(推土机前)上,每个内核每个周期都可以支持一个 SSE/AVX 加法和一个 SSE/AVX 乘法。 由于我的循环有 100 个添加,我知道我不能做得比 100 个循环更好。是的,乘法器将有一半时间处于空闲状态,但加法器是瓶颈。

      现在我开始为循环计时,每次迭代我得到 105 个循环。这意味着我非常接近最佳状态,并且没有更多的收获。但是如果我得到了 250 个循环,那么这意味着循环有问题,值得对其进行更多修改。

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

      Agner Fog 对当前处理器的内部细节有很好的参考: http://www.agner.org/optimize/microarchitecture.pdf

      【讨论】:

        【解决方案4】:

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

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

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

        【讨论】:

        • 相当不错,但不再维护了。最后支持的微架构是 Haswell。这在调整 Skylake 时仍然有用,但希望英特尔会再次开始更新它。它并不完美,有很多限制,有时它的数字与真实硬件不一致,但它绝对有用。
        猜你喜欢
        • 2018-09-04
        • 2020-03-30
        • 2012-05-06
        • 2014-12-09
        • 2011-12-09
        • 2010-10-09
        • 1970-01-01
        • 2011-12-16
        • 1970-01-01
        相关资源
        最近更新 更多