【问题标题】:AVX2 slower than SSE on HaswellAVX2 在 Haswell 上比 SSE 慢
【发布时间】:2014-06-23 05:40:25
【问题描述】:

我有以下代码(普通、SSE 和 AVX):

int testSSE(const aligned_vector & ghs, const aligned_vector & lhs) {
    int result[4] __attribute__((aligned(16))) = {0};
    __m128i vresult = _mm_set1_epi32(0);
    __m128i v1, v2, vmax;

    for (int k = 0; k < ghs.size(); k += 4) {
        v1 = _mm_load_si128((__m128i *) & lhs[k]);
        v2 = _mm_load_si128((__m128i *) & ghs[k]);
        vmax = _mm_add_epi32(v1, v2);
        vresult = _mm_max_epi32(vresult, vmax);
    }
    _mm_store_si128((__m128i *) result, vresult);
    int mymax = result[0];
    for (int k = 1; k < 4; k++) {
        if (result[k] > mymax) {
            mymax = result[k];
        }
    }
    return mymax;
}

 int testAVX(const aligned_vector & ghs, const aligned_vector & lhs) {
    int result[8] __attribute__((aligned(32))) = {0};
    __m256i vresult = _mm256_set1_epi32(0);
    __m256i v1, v2, vmax;

    for (int k = 0; k < ghs.size(); k += 8) {
        v1 = _mm256_load_si256((__m256i *) & ghs[ k]);
        v2 = _mm256_load_si256((__m256i *) & lhs[k]);
        vmax = _mm256_add_epi32(v1, v2);
        vresult = _mm256_max_epi32(vresult, vmax);
    }
    _mm256_store_si256((__m256i *) result, vresult);
    int mymax = result[0];
    for (int k = 1; k < 8; k++) {
        if (result[k] > mymax) {
            mymax = result[k];
        }
    }
    return mymax;
}

int testNormal(const aligned_vector & ghs, const aligned_vector & lhs) {
    int max = 0;
    int tempMax;
    for (int k = 0; k < ghs.size(); k++) {
        tempMax = lhs[k] + ghs[k];
        if (max < tempMax) {
            max = tempMax;
        }
    }
    return max;
}

所有这些功能都使用以下代码进行测试:

void alignTestSSE() {
    aligned_vector lhs;
    aligned_vector ghs;

    int mySize = 4096;
    int FinalResult;
    int nofTestCases = 1000;
    double time, time1, time2, time3;
    vector<int> lhs2;
    vector<int> ghs2;

    lhs.resize(mySize);
    ghs.resize(mySize);
    lhs2.resize(mySize);
    ghs2.resize(mySize);

    srand(1);
    for (int k = 0; k < mySize; k++) {
        lhs[k] = randomNodeID(1000000);
        lhs2[k] = lhs[k];
        ghs[k] = randomNodeID(1000000);
        ghs2[k] = ghs[k];
    }
    /* Warming UP */
    for (int k = 0; k < nofTestCases; k++) {
        FinalResult = testNormal(lhs, ghs);
    }

    for (int k = 0; k < nofTestCases; k++) {
        FinalResult = testSSE(lhs, ghs);
    }

    for (int k = 0; k < nofTestCases; k++) {
        FinalResult = testAVX(lhs, ghs);
    }

    cout << "===========================" << endl;
    time = timestamp();
    for (int k = 0; k < nofTestCases; k++) {
        FinalResult = testSSE(lhs, ghs);
    }
    time = timestamp() - time;
    time1 = time;
    cout << "SSE took " << time << " s" << endl;
    cout << "SSE Result: " << FinalResult << endl;

    time = timestamp();
    for (int k = 0; k < nofTestCases; k++) {
        FinalResult = testAVX(lhs, ghs);
    }
    time = timestamp() - time;
    time3 = time;
    cout << "AVX took " << time << " s" << endl;
    cout << "AVX Result: " << FinalResult << endl;



    time = timestamp();
    for (int k = 0; k < nofTestCases; k++) {
        FinalResult = testNormal(lhs, ghs);
    }
    time = timestamp() - time;
    cout << "Normal took " << time << " s" << endl;
    cout << "Normal Result: " << FinalResult << endl;
    cout << "SpeedUP SSE= " << time / time1 << " s" << endl;
    cout << "SpeedUP AVX= " << time / time3 << " s" << endl;
    cout << "===========================" << endl;
    ghs.clear();
    lhs.clear();
}

在哪里

inline double timestamp() {
    struct timeval tp;
    gettimeofday(&tp, NULL);
    return double(tp.tv_sec) + tp.tv_usec / 1000000.;
}

typedef vector<int, aligned_allocator<int, sizeof (int)> > aligned_vector;

是使用https://gist.github.com/donny-dont/1471329的AlignedAllocator对齐的向量

我有一个 intel-i7 haswell 4771,以及最新的 Ubuntu 14.04 64bit 和 gcc 4.8.2。一切都是最新的。我用 -march=native -mtune=native -O3 -m64 编译。

结果是:

SSE took 0.000375986 s
SSE Result: 1982689
AVX took 0.000459909 s
AVX Result: 1982689
Normal took 0.00315714 s
Normal Result: 1982689
SpeedUP SSE= 8.39696 s
SpeedUP AVX= 6.8647 s

这表明完全相同的代码在 AVX2 上比 SSE 慢 22%。我做错了什么还是这是正常行为?

【问题讨论】:

  • 混合 AVX 和 SSE 指令会导致开销,因为芯片必须将寄存器的上半部分归零。我强烈建议将您的 AVX 测试移动到您使用 -mavx 编译的文件中,然后在该文件中开始任何浮点之前使用内部函数调用 vzeroall
  • 只要您使用-mavx2 编译并且只使用内在函数(而不是内联汇编),那么您就不应该招致 AVX-SSE 切换惩罚。
  • 是的。即使完全删除 SSE 代码并使用 -mavx2 进行编译也不会加速代码。我也试过 _mm256_zeroall();在使用 AVX 指令之前。

标签: c++ x86 sse simd avx2


【解决方案1】:

手动进行循环展开可以加快 SSE/AVX 代码的速度。

我的 i5-5300U 上的原始版本:

Normal took 0.347 s
Normal Result: 2146591543
AVX took 0.409 s
AVX Result: 2146591543
SpeedUP AVX= 0.848411

手动循环展开后:

Normal took 0.375 s
Normal Result: 2146591543
AVX took 0.297 s
AVX Result: 2146591543
SpeedUP AVX= 1.26263

【讨论】:

    【解决方案2】:

    在我的机器(核心 i7-4900M)上,基于来自 Paul R 的更新代码,使用 g++ 4.8.2 迭代 100,000 次而不是 1000 次,我得到以下结果:

    g++ -Wall -mavx2 -O3 -std=c++11 test_avx.cpp && ./a.exe 
    SSE took             508,029 us
    AVX took           1,308,075 us
    Normal took          297,017 us
    
    
    g++ -Wall -mavx2 -O3 -std=c++11 -fno-tree-vectorize test_avx.cpp && ./a.exe 
    SSE took             509,029 us
    AVX took           1,307,075 us
    Normal took        3,436,197 us
    

    GCC 在优化“正常”代码方面做得非常出色。然而,“AVX”代码的缓慢性能可以通过以下几行来解释,这需要完整的 256 位存储(哎呀!),然后是 8 个整数的最大搜索。

    _mm256_store_si256((__m256i *) result, vresult);
    int mymax = result[0];
    for (int k = 1; k < 8; k++) {
      if (result[k] > mymax) {
         mymax = result[k];
      }
    }
    return mymax;
    

    最好继续使用 AVX 内部函数,最多 8 个。我可以提出以下更改

    v1      = _mm256_permute2x128_si256(vresult,vresult,1);  // from ABCD-EFGH to ????-ABCD
    vresult = _mm256_max_epi32(vresult, v1);
    v1      = _mm256_permute4x64_epi64(vresult,1);  // from ????-ABCD to ????-??AB
    vresult = _mm256_max_epi32(vresult, v1);
    v1      = _mm256_shuffle_epi32(vresult,1); // from ????-???AB to ????-???A
    vresult = _mm256_max_epi32(vresult, v1);
    
    // no _mm256_extract_epi32 => need extra step
    __m128i vres128 = _mm256_extracti128_si256(vresult,0);
    return _mm_extract_epi32(vres128,0);
    

    为了公平比较,我还更新了 SSE 代码,然后:

    SSE took             483,028 us
    AVX took             258,015 us
    Normal took          307,017 us
    

    AVX 时间减少了 5 倍!

    【讨论】:

    • +1 我会试试你的代码。对于数组,我还看到 GCC 显着优化了 Normal 的代码。
    • 您更新了 SSE 代码。能否也提供相应的代码?
    • v1 = _mm_shuffle_epi32(vresult,0xE); // 00_00_11_10 vresult = _mm_max_epi32(vresult, v1); v1 = _mm_shuffle_epi32(vresult,1); // 00_00_00_01 vresult = _mm_max_epi32(vresult, v1); return _mm_extract_epi32(vresult,0);
    • 谢谢,我会从那开始看看,如果我有任何改进。
    【解决方案3】:

    我将您的代码转换为更普通的 C++(纯数组、无向量等),对其进行清理并在禁用自动向量化的情况下对其进行测试,并获得了合理的结果:

    #include <iostream>
    using namespace std;
    
    #include <sys/time.h>
    #include <cstdlib>
    #include <cstdint>
    
    #include <immintrin.h>
    
    inline double timestamp() {
        struct timeval tp;
        gettimeofday(&tp, NULL);
        return double(tp.tv_sec) + tp.tv_usec / 1000000.;
    }
    
    int testSSE(const int32_t * ghs, const int32_t * lhs, size_t n) {
        int result[4] __attribute__((aligned(16))) = {0};
        __m128i vresult = _mm_set1_epi32(0);
        __m128i v1, v2, vmax;
    
        for (int k = 0; k < n; k += 4) {
            v1 = _mm_load_si128((__m128i *) & lhs[k]);
            v2 = _mm_load_si128((__m128i *) & ghs[k]);
            vmax = _mm_add_epi32(v1, v2);
            vresult = _mm_max_epi32(vresult, vmax);
        }
        _mm_store_si128((__m128i *) result, vresult);
        int mymax = result[0];
        for (int k = 1; k < 4; k++) {
            if (result[k] > mymax) {
                mymax = result[k];
            }
        }
        return mymax;
    }
    
    int testAVX(const int32_t * ghs, const int32_t * lhs, size_t n) {
        int result[8] __attribute__((aligned(32))) = {0};
        __m256i vresult = _mm256_set1_epi32(0);
        __m256i v1, v2, vmax;
    
        for (int k = 0; k < n; k += 8) {
            v1 = _mm256_load_si256((__m256i *) & ghs[k]);
            v2 = _mm256_load_si256((__m256i *) & lhs[k]);
            vmax = _mm256_add_epi32(v1, v2);
            vresult = _mm256_max_epi32(vresult, vmax);
        }
        _mm256_store_si256((__m256i *) result, vresult);
        int mymax = result[0];
        for (int k = 1; k < 8; k++) {
            if (result[k] > mymax) {
                mymax = result[k];
            }
        }
        return mymax;
    }
    
    int testNormal(const int32_t * ghs, const int32_t * lhs, size_t n) {
        int max = 0;
        int tempMax;
        for (int k = 0; k < n; k++) {
            tempMax = lhs[k] + ghs[k];
            if (max < tempMax) {
                max = tempMax;
            }
        }
        return max;
    }
    
    void alignTestSSE() {
    
        int n = 4096;
        int normalResult, sseResult, avxResult;
        int nofTestCases = 1000;
        double time, normalTime, sseTime, avxTime;
    
        int lhs[n] __attribute__ ((aligned(32)));
        int ghs[n] __attribute__ ((aligned(32)));
    
        for (int k = 0; k < n; k++) {
            lhs[k] = arc4random();
            ghs[k] = arc4random();
        }
    
        /* Warming UP */
        for (int k = 0; k < nofTestCases; k++) {
            normalResult = testNormal(lhs, ghs, n);
        }
    
        for (int k = 0; k < nofTestCases; k++) {
            sseResult = testSSE(lhs, ghs, n);
        }
    
        for (int k = 0; k < nofTestCases; k++) {
            avxResult = testAVX(lhs, ghs, n);
        }
    
        time = timestamp();
        for (int k = 0; k < nofTestCases; k++) {
            normalResult = testNormal(lhs, ghs, n);
        }
        normalTime = timestamp() - time;
    
        time = timestamp();
        for (int k = 0; k < nofTestCases; k++) {
            sseResult = testSSE(lhs, ghs, n);
        }
        sseTime = timestamp() - time;
    
        time = timestamp();
        for (int k = 0; k < nofTestCases; k++) {
            avxResult = testAVX(lhs, ghs, n);
        }
        avxTime = timestamp() - time;
    
        cout << "===========================" << endl;
        cout << "Normal took " << normalTime << " s" << endl;
        cout << "Normal Result: " << normalResult << endl;
        cout << "SSE took " << sseTime << " s" << endl;
        cout << "SSE Result: " << sseResult << endl;
        cout << "AVX took " << avxTime << " s" << endl;
        cout << "AVX Result: " << avxResult << endl;
        cout << "SpeedUP SSE= " << normalTime / sseTime << endl;
        cout << "SpeedUP AVX= " << normalTime / avxTime << endl;
        cout << "===========================" << endl;
    
    }
    
    int main()
    {
        alignTestSSE();
        return 0;
    }
    

    测试:

    $ clang++ -Wall -mavx2 -O3 -fno-vectorize SO_avx.cpp && ./a.out
    ===========================
    Normal took 0.00324106 s
    Normal Result: 2143749391
    SSE took 0.000527859 s
    SSE Result: 2143749391
    AVX took 0.000221968 s
    AVX Result: 2143749391
    SpeedUP SSE= 6.14002
    SpeedUP AVX= 14.6015
    ===========================
    

    我建议你试试上面的代码,用-fno-vectorize(或者-fno-tree-vectorize,如果使用g++),看看你是否得到类似的结果。如果你这样做了,那么你可以向后处理你的原始代码,看看不一致可能来自哪里。

    【讨论】:

    • -fno-tree-vectorize 是 gcc-only 选项,要在 clang 中禁用矢量化,您需要 -fno-vectorize
    • @ismail:它适用于我的clang++ 版本——上面是从我的终端直接复制和粘贴的。我得到与-fno-vectorize-fno-tree-vectorize 相同的行为。当然是 YMMV。
    • 感谢@PaulR 我明白了。 SSE 和 Normal 对于对齐的向量和数组的运行相同,但 AVX 对于对齐的向量要慢两倍。在我的 PC 上,带有数组的 AVX 代码比 SSE 快 1.5。因此,我可能必须重写代码以将数组用于 AVX。 +1 并接受了您的回答。谢谢
    • 大多数虚拟内存实现采用“惰性”分配方法,因此当您分配一大块内存时,只有前几页被立即分配和连接,然后其余的被处理通过页面错误。这通常是整体性能上的胜利(因为程序分配的内存通常比它们实际需要的多),但对于基准测试,您通常希望在开始计时之前连接所有这些内存。
    • 预热运行也会使 CPU 的分支预测缓存预热,因为代码非常紧凑,以至于所有分支都可能很容易适应 CPU 拥有的任何分支历史缓冲区。
    猜你喜欢
    • 2014-07-03
    • 2012-02-09
    • 1970-01-01
    • 1970-01-01
    • 2013-03-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多