【问题标题】:SSE-copy, AVX-copy and std::copy performanceSSE 复制、AVX 复制和 std::copy 性能
【发布时间】:2013-08-21 07:01:15
【问题描述】:

我试图通过 SSE 和 AVX 提高复制操作的性能:

    #include <immintrin.h>

    const int sz = 1024;
    float *mas = (float *)_mm_malloc(sz*sizeof(float), 16);
    float *tar = (float *)_mm_malloc(sz*sizeof(float), 16);
    float a=0;
    std::generate(mas, mas+sz, [&](){return ++a;});
    
    const int nn = 1000;//Number of iteration in tester loops    
    std::chrono::time_point<std::chrono::system_clock> start1, end1, start2, end2, start3, end3; 
    
    //std::copy testing
    start1 = std::chrono::system_clock::now();
    for(int i=0; i<nn; ++i)
        std::copy(mas, mas+sz, tar);
    end1 = std::chrono::system_clock::now();
    float elapsed1 = std::chrono::duration_cast<std::chrono::microseconds>(end1-start1).count();
    
    //SSE-copy testing
    start2 = std::chrono::system_clock::now();
    for(int i=0; i<nn; ++i)
    {
        auto _mas = mas;
        auto _tar = tar;
        for(; _mas!=mas+sz; _mas+=4, _tar+=4)
        {
           __m128 buffer = _mm_load_ps(_mas);
           _mm_store_ps(_tar, buffer);
        }
    }
    end2 = std::chrono::system_clock::now();
    float elapsed2 = std::chrono::duration_cast<std::chrono::microseconds>(end2-start2).count();
     
    //AVX-copy testing
    start3 = std::chrono::system_clock::now();
    for(int i=0; i<nn; ++i)
    {
        auto _mas = mas;
        auto _tar = tar;
        for(; _mas!=mas+sz; _mas+=8, _tar+=8)
        {
           __m256 buffer = _mm256_load_ps(_mas);
           _mm256_store_ps(_tar, buffer);
        }
    }
    end3 = std::chrono::system_clock::now();
    float elapsed3 = std::chrono::duration_cast<std::chrono::microseconds>(end3-start3).count();
    
    std::cout<<"serial - "<<elapsed1<<", SSE - "<<elapsed2<<", AVX - "<<elapsed3<<"\nSSE gain: "<<elapsed1/elapsed2<<"\nAVX gain: "<<elapsed1/elapsed3;
    
    _mm_free(mas);
    _mm_free(tar);

它有效。然而,虽然测试循环中的迭代次数 - nn - 增加了,但 simd-copy 的性能增益却减少了:

nn=10:SSE-gain=3,AVX-gain=6;

nn=100:SSE 增益=0.75,AVX 增益=1.5;

nn=1000:SSE 增益=0.55,AVX 增益=1.1;

谁能解释一下提到的性能下降影响的原因是什么?是否建议手动对复制操作进行矢量化?

【问题讨论】:

  • 我相信我在某处读到过(Agner Fog ?),由于 Haswell 上的积极电源管理,当您开始使用之前空闲时,可能会有一个“加速”时间(数百个周期?)执行单元,例如 SSE/AVX。对于小的 nn,这可能会扭曲您的测量结果。您应该查看绝对时间(每个元素)以及比率来验证这一点。
  • @PaulR 但是这里 SSE/AVX 变得越来越慢,而不是越来越快......这是一个下降,而不是一个上升
  • @xanatos:是的,但也许 std::copy 已经在使用 SSE/AVX,而且升级主要影响的是 std::copy,而不是随后的手动编码 SIMD 副本。您可以通过更改我想的副本的顺序来测试这一点。
  • FWIW,我无法使用 Intel Core i7 2600K 在 VS2012 上重现此问题。使用nn = 1000 太小而无法测量。上升到 nn = 1000000 显示 SSE gain: 1.02222AVX gain: 1.70371 - 如果编译器仅使用 SSE,我希望看到它本身。
  • 您的代码包含一个错误:AVX 对齐副本需要 32 字节对齐,但您只请求 16 字节对齐。另外,我认为您的测试用例的大小存在严重缺陷。在 Windows 上,如果系统时钟实现 1ms 精度,那么您很幸运,但是您的测试用例的结果在我的系统(i7-2820QM)上运行在微秒范围内。如果我在这里添加几个零,结果非常接近(~5%)。不要忘记预热你的处理器...

标签: c++ performance sse simd avx


【解决方案1】:

问题在于,您的测试在迁移硬件中一些使基准测试变得困难的因素方面做得很差。为了测试这一点,我制作了自己的测试用例。像这样的:

for blah blah:
    sleep(500ms)
    std::copy
    sse
    axv

输出:

SSE: 1.11753x faster than std::copy
AVX: 1.81342x faster than std::copy

所以在这种情况下,AVX 比std::copy 快很多。当我将测试用例更改为..时会发生什么。

for blah blah:
    sleep(500ms)
    sse
    axv
    std::copy

请注意,除了测试的顺序之外,绝对没有任何变化。

SSE: 0.797673x faster than std::copy
AVX: 0.809399x faster than std::copy

哇!这怎么可能? CPU 需要一段时间才能达到全速,因此稍后运行的测试具有优势。这个问题现在有 3 个答案,包括一个“已接受”的答案。但只有点赞数最少的那个才是正确的。

这就是基准测试很难进行的原因之一,除非他们包含详细的设置信息,否则您永远不应相信任何人的微基准测试。不仅仅是代码可能出错。省电功能和奇怪的驱动程序可能会完全搞乱您的基准测试。有一次,我通过切换 BIOS 中的开关来测量性能差异 7 倍,只有不到 1% 的笔记本电脑提供。

【讨论】:

  • 这个答案提出了一些极其重要的观点,如果没有这些观点,整个讨论将毫无用处。但恐怕这也不完全正确。它指出“CPU 需要一段时间才能达到全速”,但是,这里的问题似乎更可能与缓存有关。一个好的测试必须(至少)在一个循环中运行多次以减轻这种情况,绝不能只运行一次。
  • 那么关于“详细测试设置”,在什么操作系统和 CPU 上进行了测试?它是在 2015 年 8 月之前,所以我们知道它不是 Skylake(它引入了硬件 P 状态以更快地提升到全时钟速度)。但我们不知道您使用的是 AMD Bulldozer 还是 Intel SnB 或 Haswell 等。
  • @PeterCordes 我使用了 i7-2820QM(移动)沙桥处理器和一些桌面风格的 windows(可能是 windows 8,不确定)。
【解决方案2】:

这是一个非常有趣的问题,但是我认为到目前为止没有一个答案是正确的,因为这个问题本身就具有误导性。

标题应该改为“如何达到理论内存I/O带宽?”

无论使用什么指令集,CPU 都比 RAM 快得多,以至于纯块内存复制是 100% I/O 受限的。这就解释了为什么 SSE 和 AVX 的性能差别不大。

对于 L1D 缓存中热的小缓冲区,AVX 在 Haswell 等 CPU 上的复制速度明显快于 SSE,在 Haswell 等 CPU 上,256b 加载/存储确实使用 256b 数据路径到 L1D 缓存,而不是分成两个 128b 操作。

具有讽刺意味的是,古老的 X86 指令 rep stosq 在内存复制方面的表现比 SSE 和 AVX 好得多!

The article here 解释了如何很好地使内存带宽饱和,并且它还有丰富的参考资料可供进一步探索。

另见Enhanced REP MOVSB for memcpy 此处关于 SO,@BeeOnRope 的回答讨论了 NT 存储(以及由rep stosb/stosq 完成的非 RFO 存储)与常规存储,以及单核内存带宽通常如何受到最大并发/延迟,而不是内存控制器本身。

【讨论】:

  • rep stosq 的性能并不好,尤其是在小块和现代 CPU(2014 年之后发布)上,因为 rep stos 的初始启动成本约为 35 个周期,而在 35 个周期内你可以做 35 个使用 AVX 加载和 35 个 32 字节的存储。
  • 谢谢麦克斯!很高兴知道 2014 年之后的 CPU 被认为是现代的 :)。
【解决方案3】:

编写快速 SSE 并不像使用 SSE 操作代替它们的非并行等价物那么简单。在这种情况下,我怀疑您的编译器无法有效地展开加载/存储对,并且您的时间主要是由于在下一条指令(存储)中使用一个低吞吐量操作(加载)的输出而导致的停顿。

您可以通过手动展开一个等级来测试这个想法:

//SSE-copy testing
start2 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
{
    auto _mas = mas;
    auto _tar = tar;
    for(; _mas!=mas+sz; _mas+=8, _tar+=8)
    {
       __m128 buffer1 = _mm_load_ps(_mas);
       __m128 buffer2 = _mm_load_ps(_mas+4);
       _mm_store_ps(_tar, buffer1);
       _mm_store_ps(_tar+4, buffer2);
    }
}

通常在使用内部函数时,我会反汇编输出并确保没有发生任何疯狂的事情(您可以尝试这样做来验证原始循环是否/如何展开)。对于更复杂的循环,使用正确的工具是Intel Architecture Code Analyzer (IACA)。它是一个静态分析工具,可以告诉您“您有管道停顿”之类的信息。

【讨论】:

  • 这不是答案。 OP 没有问为什么他的 SSE/AVX 代码与 std::copy 的性能不同。他问为什么当nn 改变时性能特征会改变。
  • 这应该会有所帮助,但硬件内存重新排序已经允许它延迟存储。除非商店和 next 加载之间存在 4k 别名,否则应该没有问题。 (假设两个缓冲区相对于 4k 页面具有相同的对齐方式,内存消歧硬件可以通过查看页面偏移位来判断存储不会与以后的加载重叠。)
【解决方案4】:

我认为这是因为测量对于有点短的操作来说并不准确。

在英特尔 CPU 上测量性能时

  1. 禁用“Turbo Boost”和“SpeedStep”。您可以在系统 BIOS 上执行此操作。

  2. 将进程/线程优先级更改为高或实时。这将使您的线程保持运行。

  3. 将进程 CPU 掩码设置为仅一个内核。具有较高优先级的 CPU 掩码将最大限度地减少上下文切换。

  4. 使用__rdtsc() 内在函数。 Intel Core 系列返回 CPU 内部时钟计数器 __rdtsc()。您将从 3.4Ghz CPU 获得 3400000000 计数/秒。而__rdtsc() 会刷新 CPU 中的所有计划操作,以便更准确地测量时序。

这是我用于测试 SSE/AVX 代码的测试平台启动代码。

    int GetMSB(DWORD_PTR dwordPtr)
    {
        if(dwordPtr)
        {
            int result = 1;
    #if defined(_WIN64)
            if(dwordPtr & 0xFFFFFFFF00000000) { result += 32; dwordPtr &= 0xFFFFFFFF00000000; }
            if(dwordPtr & 0xFFFF0000FFFF0000) { result += 16; dwordPtr &= 0xFFFF0000FFFF0000; }
            if(dwordPtr & 0xFF00FF00FF00FF00) { result += 8;  dwordPtr &= 0xFF00FF00FF00FF00; }
            if(dwordPtr & 0xF0F0F0F0F0F0F0F0) { result += 4;  dwordPtr &= 0xF0F0F0F0F0F0F0F0; }
            if(dwordPtr & 0xCCCCCCCCCCCCCCCC) { result += 2;  dwordPtr &= 0xCCCCCCCCCCCCCCCC; }
            if(dwordPtr & 0xAAAAAAAAAAAAAAAA) { result += 1; }
    #else
            if(dwordPtr & 0xFFFF0000) { result += 16; dwordPtr &= 0xFFFF0000; }
            if(dwordPtr & 0xFF00FF00) { result += 8;  dwordPtr &= 0xFF00FF00; }
            if(dwordPtr & 0xF0F0F0F0) { result += 4;  dwordPtr &= 0xF0F0F0F0; }
            if(dwordPtr & 0xCCCCCCCC) { result += 2;  dwordPtr &= 0xCCCCCCCC; }
            if(dwordPtr & 0xAAAAAAAA) { result += 1; }
    #endif
            return result;
        }
        else
        {
            return 0;
        }
    }

    int _tmain(int argc, _TCHAR* argv[])
    {
        // Set Core Affinity
        DWORD_PTR processMask, systemMask;
        GetProcessAffinityMask(GetCurrentProcess(), &processMask, &systemMask);
        SetProcessAffinityMask(GetCurrentProcess(), 1 << (GetMSB(processMask) - 1) );
    
        // Set Process Priority. you can use REALTIME_PRIORITY_CLASS.
        SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
    
        DWORD64 start, end;
        start = __rdtsc();
    // your code here.
        end = __rdtsc();
        printf("%I64d\n", end - start);
        return 0;
    }

【讨论】:

  • 请注意:rdtsc() 返回自基准时钟速度某个时间点以来的时钟周期数。如果您的 CPU 具有涡轮增压或省电功能,这将不会返回您期望的结果。运行此类基准测试时,请考虑使用油门停止将 CPU 锁定在其基本频率。
  • @Stefan,什么是油门停止?这听起来像是我想雇用的东西。
  • Throttlestop 是一个简单的严肃工具,可让您控制 CPU 的时钟速度,因为支持 C2D,甚至是移动 CPU,所有 CPU 都支持。 thedigitalhq.com/downloads/download-info/throttlestop-6-00。通常,您希望在运行基准测试时始终使用它来消除尽可能多的变量。它仅适用于 Windows。
【解决方案5】:

我认为您的主要问题/瓶颈是您的_mm_malloc

如果您担心 C++ 中的局部性,我强烈建议您使用 std::vector 作为您的主要数据结构。

intrinsics 不完全是“库”,它们更像是编译器提供给您的 builtin 函数,您应该熟悉编译器内部/文档在使用此功能之前。

还要注意AVXSSE 更新这一事实并不能使AVX 更快,无论您打算使用什么,函数所占用的周期数可能比“avx vs sse”参数,例如参见this answer

尝试使用 POD int array[]std::vector

【讨论】:

  • 您推荐std::vector,一种无法控制对齐的数据结构,用于使用要求正确对齐的指令的测试用例?此外,您的 _mm_malloc 来源特别关注自动矢量化器。如果_mm_malloc 没有没有按预期工作,_mm_load_ps 应该产生一个中断。
  • @Stefan 我建议是因为缓存,而不是因为对齐,而且我想不出一个容器可以自动为您提供正确的对齐,很可能您必须与您的T 合作才能获得所需的适当对齐。此外,我的回答从未提及这些东西,显然是针对内存、缓存和分配的,我看不出您的评论与我的回答有何关联。
  • 我真的不明白使用std::vector 而不是_mm_malloc 对缓存或本地化有何帮助。更不用说在这个测试用例中它可能是一个“瓶颈”。 _mm_malloc 只是 new 的包装。
  • @Stefan 认真阅读我的帖子再发表评论,std::vector 是关于地方性的部分,这在我的帖子中已明确表达。
  • 你的帖子,在我看来,清楚地表达了他的主要问题是_mm_malloc,如果你关心地方性,你推荐std::vector,我在这里错过了什么?
猜你喜欢
  • 2015-01-05
  • 2016-10-29
  • 2020-08-06
  • 1970-01-01
  • 2013-02-05
  • 2021-08-25
  • 2022-01-14
  • 1970-01-01
  • 2017-05-28
相关资源
最近更新 更多