【问题标题】:memcpy beats SIMD intrinsicsmemcpy 胜过 SIMD 内在函数
【发布时间】:2020-12-26 03:45:52
【问题描述】:

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

我进行了一些基准测试,并得到了一些有趣的结果。我试图理解我在看什么。

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

1。基线

逐个复制元素:

for (int i = 0; i < size; ++i)
{
    copy[i] = orig[i];
}

2。霓虹灯

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

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

int32x4_t tmp;
for (int i = 0; i < size; i += 4)
{
    tmp = vld1q_s32(orig + i); // load 4 elements to tmp SIMD register
    vst1q_s32(&copy2[i], tmp); // copy 4 elements from tmp SIMD register
}

3。踩memcpy,

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

for (int i = 0; i < size; i+=4)
{
    memcpy(orig+i, copy3+i, 4);
}

4。普通memcpy

使用memcpy 处理全部数据。

memcpy(orig, copy4, size);

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

1. Baseline time = 3443[µs]
2. NEON time = 1682[µs]
3. memcpy (stepped) time = 1445[µs]
4. memcpy time = 81[µs]

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

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

This question 讨论了 NEON 与 memcpy()。但是我觉得答案并没有充分探讨为什么 ARM memcpy 实现运行得这么好

完整的代码清单如下:

#include <arm_neon.h>
#include <vector>
#include <cinttypes>

#include <iostream>
#include <cstdlib>
#include <chrono>
#include <cstring>

int main(int argc, char *argv[]) {

    int arr_size;
    if (argc==1)
    {
        std::cout << "Please enter an array size" << std::endl;
        exit(1);
    }

    int size =  atoi(argv[1]); // not very C++, sorry
    std::int32_t* orig = new std::int32_t[size];
    std::int32_t* copy = new std::int32_t[size];
    std::int32_t* copy2 = new std::int32_t[size];
    std::int32_t* copy3 = new std::int32_t[size];
    std::int32_t* copy4 = new std::int32_t[size];


    // Non-neon version
    std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
    for (int i = 0; i < size; ++i)
    {
        copy[i] = orig[i];
    }
    std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
    std::cout << "Baseline time = " << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << "[µs]" << std::endl;

    // NEON version
    begin = std::chrono::steady_clock::now();
    int32x4_t tmp;
    for (int i = 0; i < size; i += 4)
    {
        tmp = vld1q_s32(orig + i); // load 4 elements to tmp SIMD register
        vst1q_s32(&copy2[i], tmp); // copy 4 elements from tmp SIMD register
    }
    end = std::chrono::steady_clock::now();
    std::cout << "NEON time = " << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << "[µs]" << std::endl;


    // Memcpy example
    begin = std::chrono::steady_clock::now();
    for (int i = 0; i < size; i+=4)
    {
        memcpy(orig+i, copy3+i, 4);
    }
    end = std::chrono::steady_clock::now();
    std::cout << "memcpy time = " << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << "[µs]" << std::endl;


    // Memcpy example
    begin = std::chrono::steady_clock::now();
    memcpy(orig, copy4, size);
    end = std::chrono::steady_clock::now();
    std::cout << "memcpy time = " << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << "[µs]" << std::endl;

    return 0;
}

【问题讨论】:

  • 人们喜欢优化 memcpy。我希望它会很快。
  • Plain memcpy 调用复制的数据比其他方法少 4 倍。它以字节为单位,但您传递 4 字节元素的数量。假设该调用甚至可以编译 - 它指的是一个名称 i,它似乎没有在所示代码的任何地方声明。而且你传递参数的方式错误:第一个是副本的目标,第二个是源。
  • 此外,如果您正在优化您的程序,它可能会消除死代码,因为它似乎实际上并没有做任何事情。如果你不优化它,所有的数字都是毫无价值的,因为库中的 memcpy 肯定是优化的。
  • 您可能希望使用基准库,您可以在其中使用-Ofast-O3 编译您的测试对象文件,以及使用-O0 的基准测试主函数
  • 你所谓的“stepped memcpy”也只复制每4个元素,而且方向错误。

标签: c++ performance arm simd intrinsics


【解决方案1】:

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

因为“正常 memcpy”测试最后发生,所以 dead code elimination 将解释与其他测试相比的巨大数量级加速。优化器看到orig在最后一个memcpy之后没有被使用,所以它消除了这个memcpy。

编写可靠基准的好方法是使用Benchmark 框架,并使用它们的benchmark::DoNotOptimize(x) 函数防止死代码消除。

【讨论】:

    猜你喜欢
    • 2015-11-11
    • 1970-01-01
    • 1970-01-01
    • 2012-11-05
    • 2020-12-12
    • 1970-01-01
    • 1970-01-01
    • 2011-06-25
    • 2016-04-26
    相关资源
    最近更新 更多