SSE拷贝、AVX拷贝和std::拷贝性能

SSE-copy, AVX-copy and std::copy performance

本文关键字:拷贝 性能 std AVX SSE      更新时间:2023-10-16

我试图通过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副本的性能增益降低:

nn=10:SSE增益=3,AVX增益=6;

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

nn=1000:SSE增益=0.55,AVX增益=1.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%的笔记本电脑提供这种差异。

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

标题应更改为"如何达到理论内存I/O带宽?"

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

对于L1D缓存中的小缓冲区,AVX可以比Haswell等CPU上的SSE复制快得多,在Haswell中,256b加载/存储确实使用了到L1D缓存的256b数据路径,而不是拆分为两个128b操作。

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

这里的文章解释了如何很好地饱和内存带宽,它也有丰富的参考资料可以进一步探索。

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

编写快速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);
    }
}

通常,当使用内部函数时,我会分解输出,并确保没有发生任何疯狂的事情(你可以尝试验证原始循环是否/如何展开)。对于更复杂的循环,正确的工具是"英特尔体系结构代码分析器"(IACA)。这是一个静态分析工具,可以告诉你"你有管道停滞"之类的事情。

我认为这是因为测量对于一些短操作来说不准确。

在英特尔CPU 上测量性能时

  1. 禁用";Turbo Boost";以及";SpeedStep";。您可以在系统BIOS上进行此操作。

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

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

  4. 使用CCD_ 3内在函数。Intel Core系列返回带有__rdtsc()的CPU内部时钟计数器。您将从3.4Ghz CPU获得340000000计数/秒。__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("%I64dn", end - start);
        return 0;
    }

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

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

内部函数并不完全是一个"库",它们更像是编译器提供给您的内置函数,在使用这些函数之前,您应该熟悉编译器内部/文档。

还要注意的是,AVXSSE新这一事实并不能使AVX更快,无论您计划使用什么,函数所用的周期数可能比"avx vs sse"参数更重要,例如,请参阅此答案。

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