memcpy 击败 SIMD 内在函数

Pru*_*ica 0 c++ performance arm simd intrinsics

当 NEON 向量指令在 ARM 设备上可用时,我一直在寻找复制各种数据量的快速方法。

\n

我做了一些基准测试,并得到了一些有趣的结果。我试图理解我所看到的东西。

\n

我有四个版本来复制数据:

\n

1. 基线

\n

逐个元素复制:

\n
for (int i = 0; i < size; ++i)\n{\n    copy[i] = orig[i];\n}\n
Run Code Online (Sandbox Code Playgroud)\n

2. 霓虹灯

\n

此代码将四个值加载到临时寄存器中,然后将该寄存器复制到输出。

\n

因此,负载数量减少了一半。可能有一种方法可以跳过临时寄存器并将负载减少四分之一,但我还没有找到方法。

\n
int32x4_t tmp;\nfor (int i = 0; i < size; i += 4)\n{\n    tmp = vld1q_s32(orig + i); // load 4 elements to tmp SIMD register\n    vst1q_s32(&copy2[i], tmp); // copy 4 elements from tmp SIMD register\n}\n
Run Code Online (Sandbox Code Playgroud)\n

3. 阶梯式memcpy,

\n

使用memcpy,但一次复制 4 个元素。这是为了与 NEON 版本进行比较。

\n
for (int i = 0; i < size; i+=4)\n{\n    memcpy(orig+i, copy3+i, 4);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

4. 正常memcpy

\n

memcpy与全量数据一起使用。

\n
memcpy(orig, copy4, size);\n
Run Code Online (Sandbox Code Playgroud)\n

我使用2^16值进行的基准测试给出了一些令人惊讶的结果:

\n
1. Baseline time = 3443[\xc2\xb5s]\n2. NEON time = 1682[\xc2\xb5s]\n3. memcpy (stepped) time = 1445[\xc2\xb5s]\n4. memcpy time = 81[\xc2\xb5s]\n
Run Code Online (Sandbox Code Playgroud)\n
\n

NEON 时间的加速是预期的,但更快的步进memcpy时间令我惊讶。时间更是如此4

\n

为什么memcpy做得这么好?它在引擎盖下使用 NEON 吗?或者是否存在我不知道的有效内存复制指令?

\n

这个问题讨论了 NEON 与memcpy(). 然而,我认为答案并没有充分探讨为什么 ARMmemcpy实现运行得这么好

\n

完整的代码清单如下:

\n
#include <arm_neon.h>\n#include <vector>\n#include <cinttypes>\n\n#include <iostream>\n#include <cstdlib>\n#include <chrono>\n#include <cstring>\n\nint main(int argc, char *argv[]) {\n\n    int arr_size;\n    if (argc==1)\n    {\n        std::cout << "Please enter an array size" << std::endl;\n        exit(1);\n    }\n\n    int size =  atoi(argv[1]); // not very C++, sorry\n    std::int32_t* orig = new std::int32_t[size];\n    std::int32_t* copy = new std::int32_t[size];\n    std::int32_t* copy2 = new std::int32_t[size];\n    std::int32_t* copy3 = new std::int32_t[size];\n    std::int32_t* copy4 = new std::int32_t[size];\n\n\n    // Non-neon version\n    std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();\n    for (int i = 0; i < size; ++i)\n    {\n        copy[i] = orig[i];\n    }\n    std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();\n    std::cout << "Baseline time = " << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << "[\xc2\xb5s]" << std::endl;\n\n    // NEON version\n    begin = std::chrono::steady_clock::now();\n    int32x4_t tmp;\n    for (int i = 0; i < size; i += 4)\n    {\n        tmp = vld1q_s32(orig + i); // load 4 elements to tmp SIMD register\n        vst1q_s32(&copy2[i], tmp); // copy 4 elements from tmp SIMD register\n    }\n    end = std::chrono::steady_clock::now();\n    std::cout << "NEON time = " << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << "[\xc2\xb5s]" << std::endl;\n\n\n    // Memcpy example\n    begin = std::chrono::steady_clock::now();\n    for (int i = 0; i < size; i+=4)\n    {\n        memcpy(orig+i, copy3+i, 4);\n    }\n    end = std::chrono::steady_clock::now();\n    std::cout << "memcpy time = " << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << "[\xc2\xb5s]" << std::endl;\n\n\n    // Memcpy example\n    begin = std::chrono::steady_clock::now();\n    memcpy(orig, copy4, size);\n    end = std::chrono::steady_clock::now();\n    std::cout << "memcpy time = " << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << "[\xc2\xb5s]" << std::endl;\n\n    return 0;\n}\n\n
Run Code Online (Sandbox Code Playgroud)\n

Pas*_*uer 6

注意:此代码以错误的方向使用 memcpy。它应该是memcpy(dest, src, num_bytes)

因为“正常 memcpy”测试最后发生,所以与其他测试相比,巨大数量级的加速可以通过死代码消除来解释。优化器发现orig在最后一个memcpy之后没有使用它,所以它消除了memcpy。

编写可靠基准测试的一个好方法是使用Benchmark框架,并使用它们的benchmark::DoNotOptimize(x)功能来防止死代码消除。