Windows 和 Linux 之间使用 SIMD 的代码速度差异

Speed difference of code using SIMD between Windows and Linux

提问人:Gendai 提问时间:11/2/2023 最后编辑:Gendai 更新时间:11/2/2023 访问量:118

问:

我正在做一个项目,我第一次尝试使用矢量化来加快计算时间。 总体思路是给出一个足够大的数组,应用一些按位掩码,并计算具有位奇偶校验的uint16_t数。 以下代码是检查性能的测试用例,它生成给定大小的随机数据,然后执行对 std::transform 的调用。

#include <map>
#include <vector>
#include <execution>
#include <immintrin.h>
#include <cstdint>
#include <chrono>
#include <random>
#include <memory>

void test_si128()
{
    std::random_device dev;
    std::mt19937 rng(dev());
    std::uniform_int_distribution<std::mt19937::result_type> dist6(0, 8192);

    const int kVectorSize = 32767;
    const unsigned long sectionSize = ((kVectorSize + 1) / 2) / 8;

    std::vector<std::vector<std::uint16_t>> vectorTest;
    vectorTest.resize(kVectorSize);
    for (int d0 = 0; d0 < kVectorSize; ++d0)
    {
        for (int d1 = d0 + 1; d1 < kVectorSize + 1; ++d1)
        {
            const int index = d0 ^ d1;
            vectorTest[index - 1].push_back(static_cast<std::uint16_t>(dist6(rng)));
        }
    }

    __m128i** masks = new __m128i*[kVectorSize];
    int i = 0;
    for (const auto& innerList : vectorTest)
    {
        __m128i* sectionPtr = new __m128i[sectionSize];
        for (auto section = 0; section < sectionSize; ++section)
        {
            sectionPtr[section] = _mm_loadu_si128((__m128i*)(&innerList[0] + (section * 8)));
        }
        masks[i] = sectionPtr;
        i++;
    }
    unsigned long* pRes = new unsigned long[kVectorSize];

    // Tests masks
    const __m128i oneBuf = _mm_set_epi16(1, 1, 1, 1, 1, 1, 1, 1);
    const __m128i sectionMask = _mm_set_epi16(6, 6, 6, 6, 6, 6, 6, 6);

    auto startChrono = std::chrono::high_resolution_clock::now();

    std::transform(std::execution::par_unseq, masks, masks + kVectorSize, pRes,
        [&sectionMask = std::as_const(sectionMask), &oneBuf = std::as_const(oneBuf), &sectionSize = std::as_const(sectionSize)](const __m128i* const& list) {
            __m128i accu = _mm_setzero_si128();
            for (auto section = 0; section < sectionSize; ++section)
            {
                const __m128i res = _mm_and_si128(list[section], sectionMask);
                __m128i y = _mm_srli_epi16(res, 1);
                y = _mm_xor_si128(res, y);
                __m128i yTmp = _mm_srli_epi16(y, 2);
                yTmp = _mm_xor_si128(y, yTmp);
                y = _mm_srli_epi16(yTmp, 4);
                yTmp = _mm_xor_si128(y, yTmp);
                y = _mm_srli_si128(yTmp, 1);
                y = _mm_xor_si128(yTmp, y);
                y = _mm_and_si128(y, oneBuf);
                accu = _mm_add_epi16(accu, y);
            }
            const unsigned long red =
                _mm_extract_epi16(accu, 0)
                + _mm_extract_epi16(accu, 1)
                + _mm_extract_epi16(accu, 2)
                + _mm_extract_epi16(accu, 3)
                + _mm_extract_epi16(accu, 4)
                + _mm_extract_epi16(accu, 5)
                + _mm_extract_epi16(accu, 6)
                + _mm_extract_epi16(accu, 7);
            return red;
    });

    auto endChrono = std::chrono::high_resolution_clock::now();

    std::cout << "Time: " << std::chrono::duration_cast<std::chrono::milliseconds>(endChrono - startChrono).count() << " milliseconds\n";
}

int main(int argc, char** argv)
{
    test_si128();
}

我的问题是,在同一台机器(x64,I5-13600k)上提供了相同的精确代码,我看到在Windows上使用MSVC(2022)和linux使用g++(11.4.0)编译的代码之间存在非常明显的速度差异。 在这两种情况下,我都在版本中编译,启用了 AVX 的 C++17 并进行了全面优化。 在 Windows 上,此代码在大约 ~20 毫秒内完成,在 Linux 上需要 ~90 毫秒。

速度差异似乎来自直接在 128 位上执行的内部函数,因为当我评论它们时,代码在 Linux 上运行得更快,并且达到比在 Windows 上相似的性能。我想到了一个内存对齐问题,但即使使用 std::align_alloc 来分配 sectionPtr,执行时间也是相同的。

Windows 和 Linux 之间是否存在这种速度差异?

C++ x86-64 SIMD SSE

评论

1赞 Pepijn Kramer 11/2/2023
如果在 linux 上默认为 .为了获得一致的结果,请使用(系统时钟可以在时间上倒退...真的可以)std::chrono::high_resolution_clockstd::chrono::system_clockstd::chrono::steady_clock
1赞 Gendai 11/2/2023
@drescherjm是的,在两个系统上运行优化的代码。我使用 WSL2 进行了测试,但一位朋友也在实际的 Linux 机器上进行了测试,并在执行顺序上得到了相同的结果。
4赞 Alan Birtles 11/2/2023
您的代码为我崩溃(一旦我添加了编译所需的缺少标头)。您确定这是一个最小的可重现示例吗?godbolt.org/z/oGvj4baz3
1赞 Gendai 11/2/2023
@Peter Cordes,我用 std::uint32_t 替换了 unsigned long,但结果相同。关于_mm_extract_epi16,是的,我会使用_mm_hadd_epi16但它在应该运行此代码的最终目标计算机上不可用。
2赞 Peter Cordes 11/2/2023
速度差异似乎来自直接在 128 位上执行的内部函数,因为当我评论它们时,代码在 Linux 上运行得更快,并且达到比在 Windows 上相似的性能。- 等等,你试图匹配的时间是空循环吗?听起来 MSVC 只是优化了循环体,因为您不使用结果,例如通过打印它。(令人惊讶;我本来以为 MSVC 会是不那么激进的编译器。不过,没有 clang 那么激进,它也优化了整个循环。

答: 暂无答案