memcpy击败了SIMD内部函数 1基线 2霓虹灯 3踩过memcpy, 4普通memcpy

问题描述

当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,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,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。应该是memcpy(dest,src,num_bytes)

由于“正常memcpy”测试最后一次发生,因此dead code elimination可以解释与其他测试相比数量级加速的数量级。优化程序发现在上一个memcpy之后没有使用orig,因此它消除了memcpy。

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

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...