20

我试图通过 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 增益=3,AVX 增益=6;

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

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

任何人都可以解释提到的性能下降影响的原因是什么,是否建议手动对复制操作进行矢量化?

4

5 回答 5

24

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

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 个答案,包括一个“已接受”的答案。但只有投票数最少的那个才是正确的。

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

于 2014-06-05T20:40:28.433 回答
8

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

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

无论使用什么指令集,CPU 都比 RAM 快得多,以至于纯块内存副本是 100% I/O 受限的。这解释了为什么 SSE 和 AVX 性能之间几乎没有差异。

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

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

此处的文章解释了如何很好地使内存带宽饱和,并且还提供了丰富的参考资料供进一步探索。

另请参阅SO 上memcpy 的增强型 REP MOVSB,其中@BeeOnRope 的回答讨论了 NT 存储(以及rep stosb/stosq由内存控制器本身。

于 2015-01-19T17:28:24.320 回答
3

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

在 Intel CPU 上测量性能时

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

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

  3. 将 Process CPU Mask 设置为仅一个内核。具有较高优先级的 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;
    }
于 2013-08-20T06:25:34.197 回答
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);
    }
}

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

于 2013-08-19T17:55:41.640 回答
-1

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

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

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

另请注意,无论您打算使用什么,函数所占用的周期数可能AVX比“avx vs sse”参数更重要,例如看到这个回答SSEAVX

尝试使用 PODint array[]std::vector.

于 2013-08-19T17:44:14.283 回答