更快的替代memcpy?

时间:2010-06-03 07:05:33

标签: c performance memcpy

我有一个正在执行memcpy的功能,但它占用了大量的周期。有没有比使用memcpy移动内存更快的替代/方法?

18 个答案:

答案 0 :(得分:118)

memcpy可能是您在内存中复制字节的最快方法。如果您需要更快的东西 - 尝试找出复制内容的方式,例如:只交换指针,而不是数据本身。

答案 1 :(得分:15)

这是x86_64的答案,其中存在AVX2指令集。虽然类似的东西可能适用于带有SIMD的ARM / AArch64。

在完全填充单个内存通道的Ryzen 1800X上(2个插槽,每个16 GB DDR4),以下代码比MSVC ++ 2017编译器上的memcpy()快1.56倍。如果您使用2个DDR4模块填充两个内存通道,即所有4个DDR4插槽都忙,则可能会使内存复制速度提高2倍。对于三(四)通道存储器系统,如果代码扩展到类似的AVX512代码,则可以进一步加快1.5(2.0)倍的存储器复制。对于AVX2而言,所有插槽都忙的三/四通道系统预计不会更快,因为要完全加载它们需要一次加载/存储超过32个字节(四通道为48字节,四通道为64字节)系统),而AVX2可以一次加载/存储不超过32个字节。虽然某些系统上的多线程可以在没有AVX512甚至AVX2的情况下缓解这种情况。

所以这里是复制代码,假设你正在复制一个大块的内存块,其大小是32的倍数,并且块是32字节对齐的。

对于非多个大小和非对齐块,可以写入序言/结尾代码,将块宽和尾部的宽度减小到16(SSE4.1),8,4,2,最后一个字节。同样在中间,2-3个__m256i值的本地数组可用作来自源的对齐读取和到目标的对齐写入之间的代理。

#include <immintrin.h>
#include <cstdint>
/* ... */
void fastMemcpy(void *pvDest, void *pvSrc, size_t nBytes) {
  assert(nBytes % 32 == 0);
  assert((intptr_t(pvDest) & 31) == 0);
  assert((intptr_t(pvSrc) & 31) == 0);
  const __m256i *pSrc = reinterpret_cast<const __m256i*>(pvSrc);
  __m256i *pDest = reinterpret_cast<__m256i*>(pvDest);
  int64_t nVects = nBytes / sizeof(*pSrc);
  for (; nVects > 0; nVects--, pSrc++, pDest++) {
    const __m256i loaded = _mm256_stream_load_si256(pSrc);
    _mm256_stream_si256(pDest, loaded);
  }
  _mm_sfence();
}

此代码的一个关键特性是它在复制时跳过CPU缓存:当涉及CPU缓存时(即使用不带_stream_的AVX指令),复制速度在我的系统上会下降几次。

我的DDR4内存为2.6GHz CL13。因此,当将8GB数据从一个阵列复制到另一个阵列时,我得到了以下速度:

memcpy(): 17 208 004 271 bytes/sec.
Stream copy: 26 842 874 528 bytes/sec.

请注意,在这些测量中,输入和输出缓冲区的总大小除以经过的秒数。因为对于数组的每个字节,有2个存储器访问:一个用于从输入数组读取字节,另一个用于将字节写入输出数组。换句话说,当从一个阵列复制8GB到另一个阵列时,你可以进行16GB的内存访问操作。

中等多线程可以进一步提高1.44倍的性能,因此在我的机器上总增长超过memcpy()达到2.55倍。 以下是流复制性能如何取决于我的机器上使用的线程数:

Stream copy 1 threads: 27114820909.821 bytes/sec
Stream copy 2 threads: 37093291383.193 bytes/sec
Stream copy 3 threads: 39133652655.437 bytes/sec
Stream copy 4 threads: 39087442742.603 bytes/sec
Stream copy 5 threads: 39184708231.360 bytes/sec
Stream copy 6 threads: 38294071248.022 bytes/sec
Stream copy 7 threads: 38015877356.925 bytes/sec
Stream copy 8 threads: 38049387471.070 bytes/sec
Stream copy 9 threads: 38044753158.979 bytes/sec
Stream copy 10 threads: 37261031309.915 bytes/sec
Stream copy 11 threads: 35868511432.914 bytes/sec
Stream copy 12 threads: 36124795895.452 bytes/sec
Stream copy 13 threads: 36321153287.851 bytes/sec
Stream copy 14 threads: 36211294266.431 bytes/sec
Stream copy 15 threads: 35032645421.251 bytes/sec
Stream copy 16 threads: 33590712593.876 bytes/sec

代码是:

void AsyncStreamCopy(__m256i *pDest, const __m256i *pSrc, int64_t nVects) {
  for (; nVects > 0; nVects--, pSrc++, pDest++) {
    const __m256i loaded = _mm256_stream_load_si256(pSrc);
    _mm256_stream_si256(pDest, loaded);
  }
}

void BenchmarkMultithreadStreamCopy(double *gpdOutput, const double *gpdInput, const int64_t cnDoubles) {
  assert((cnDoubles * sizeof(double)) % sizeof(__m256i) == 0);
  const uint32_t maxThreads = std::thread::hardware_concurrency();
  std::vector<std::thread> thrs;
  thrs.reserve(maxThreads + 1);

  const __m256i *pSrc = reinterpret_cast<const __m256i*>(gpdInput);
  __m256i *pDest = reinterpret_cast<__m256i*>(gpdOutput);
  const int64_t nVects = cnDoubles * sizeof(*gpdInput) / sizeof(*pSrc);

  for (uint32_t nThreads = 1; nThreads <= maxThreads; nThreads++) {
    auto start = std::chrono::high_resolution_clock::now();
    lldiv_t perWorker = div((long long)nVects, (long long)nThreads);
    int64_t nextStart = 0;
    for (uint32_t i = 0; i < nThreads; i++) {
      const int64_t curStart = nextStart;
      nextStart += perWorker.quot;
      if ((long long)i < perWorker.rem) {
        nextStart++;
      }
      thrs.emplace_back(AsyncStreamCopy, pDest + curStart, pSrc+curStart, nextStart-curStart);
    }
    for (uint32_t i = 0; i < nThreads; i++) {
      thrs[i].join();
    }
    _mm_sfence();
    auto elapsed = std::chrono::high_resolution_clock::now() - start;
    double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
    printf("Stream copy %d threads: %.3lf bytes/sec\n", (int)nThreads, cnDoubles * 2 * sizeof(double) / nSec);

    thrs.clear();
  }
}

答案 2 :(得分:11)

请向我们提供更多详情。在i386架构上,memcpy很可能是最快的复制方式。但是在编译器没有优化版本的不同架构上,最好重写memcpy函数。我是使用汇编语言在自定义ARM架构上完成的。如果您传输大块内存,那么DMA可能就是您正在寻找的答案。

请提供更多详细信息 - 架构,操作系统(如果相关)。

答案 3 :(得分:6)

通常,编译器附带的标准库将以目标平台的最快方式实现memcpy()

答案 4 :(得分:6)

实际上,memcpy不是最快的方式,特别是如果你多次调用它。我还有一些我真正需要加速的代码,而memcpy很慢,因为它有太多不必要的检查。例如,它检查目标和源内存块是否重叠,以及是否应该从块后面而不是前面开始复制。如果你不关心这些考虑因素,你当然可以做得更好。我有一些代码,但这可能是一个更好的版本:

Very fast memcpy for image processing?

如果您搜索,您也可以找到其他实现。但是对于真正的速度,你需要一个装配版本。

答案 5 :(得分:4)

Agner Fog实施快速memcpy http://www.agner.org/optimize/#asmlib

答案 6 :(得分:3)

根本不制作副本通常会更快。是否可以调整你的功能而不是复制我不知道但是值得一试。

答案 7 :(得分:3)

有时像memcpy,memset等函数以两种不同的方式实现:

  • 曾经是一个真正的功能
  • 曾经作为一个立即内联的集会

默认情况下,并非所有编译器都采用内联汇编版本,您的编译器可能会默认使用函数变量,因为函数调用会导致一些开销。 检查你的编译器,看看如何采用函数的内在变体(命令行选项,pragma,...)。

编辑:有关Microsoft C编译器的内在函数的说明,请参阅http://msdn.microsoft.com/en-us/library/tzkfha43%28VS.80%29.aspx

答案 8 :(得分:2)

检查编译器/平台手册。对于某些微处理器和使用memcpy的DSP套件比intrinsic functionsDMA操作慢得多。

答案 9 :(得分:2)

如果您的平台支持它,请查看您是否可以使用mmap()系统调用将数据保留在文件中...通常,操作系统可以更好地管理。并且,正如每个人都说的那样,尽可能避免复制;在这种情况下,指针是你的朋友。

答案 10 :(得分:1)

您可能想看看这个:

http://www.danielvik.com/2010/02/fast-memcpy-in-c.html

我想尝试的另一个想法是使用COW技术复制内存块,让操作系统在页面写入后立即处理复制。这里有一些提示使用mmap()Can I do a copy-on-write memcpy in Linux?

答案 11 :(得分:1)

如果指针(输入参数)之一未与32位对齐,则此函数可能导致数据异常中止。

答案 12 :(得分:1)

您应该检查为您的代码生成的汇编代码。你不想要的是让memcpy调用生成对标准库中memcpy函数的调用 - 你想要的是重复调用最好的ASM指令来复制最大量的数据 - 例如rep movsq

你怎么能做到这一点?好吧,只要知道应该复制多少数据,编译器就会用简单memcpy替换它来优化对mov的调用。如果您编写一个具有明确(memcpy)值的constexpr,则可以看到此信息。如果编译器不知道该值,则必须回退到memcpy的字节级实现 - 问题是memcpy必须遵守单字节粒度。它仍然会一次移动128位,但在每个128b之后,它必须检查它是否有足够的数据复制为128b,或者它必须回落到64位,然后再回到32和8(我认为16可能不是最理想的)无论如何,但我不确定。)

所以你想要的是能够通过编译器可以优化的const表达式告诉memcpy数据大小是什么。这样就不会调用memcpy。您不想要的是将memcpy传递给仅在运行时才知道的变量。这转化为函数调用和大量测试以检查最佳复制指令。有时,出于这个原因,一个简单的for循环优于memcpy(消除一个函数调用)。而你真的不想要会传递给memcpy一个奇数个字节来复制。

答案 13 :(得分:0)

以下是一些基准测试Visual C ++ / Ryzen 1700。

基准测试从128个MiB环形缓冲区中复制了16个KiB(不重叠)数据块8 * 8192次(总共,复制了1 GiB数据)。

然后我将结果归一化,在这里我们以毫秒为单位显示挂钟时间,并给出60 Hz的吞吐量值(即该函数在16.667毫秒内可以处理多少数据)。

memcpy                           2.761 milliseconds ( 772.555 MiB/frame)

您可以看到内置的memcpy很快,但是快了多少?

64-wide load/store              39.889 milliseconds (  427.853 MiB/frame)
32-wide load/store              33.765 milliseconds (  505.450 MiB/frame)
16-wide load/store              24.033 milliseconds (  710.129 MiB/frame)
 8-wide load/store              23.962 milliseconds (  712.245 MiB/frame)
 4-wide load/store              22.965 milliseconds (  743.176 MiB/frame)
 2-wide load/store              22.573 milliseconds (  756.072 MiB/frame)
 1-wide load/store              35.032 milliseconds (  487.169 MiB/frame)

以上只是下面带有n变体的代码。

// n is the "wideness" from the benchmark

auto src = (__m128i*)get_src_chunk();
auto dst = (__m128i*)get_dst_chunk();

for (int32_t i = 0; i < (16 * 1024) / (16 * n); i += n) {
  __m128i temp[n];

  for (int32_t i = 0; i < n; i++) {
    temp[i] = _mm_loadu_si128(dst++);
  }

  for (int32_t i = 0; i < n; i++) {
    _mm_store_si128(src++, temp[i]);
  }
}

这些是我对结果的最佳猜测。根据我对Zen微体系结构的了解,每个周期只能获取32个字节。这就是为什么我们以2x 16字节加载/存储空间最大化的原因。

  • 1x将字节加载到128位xmm0
  • 2x将字节加载到256位ymm0

这就是为什么它速度快大约两倍的原因,并且在内部完全是memcpy的工作(或者如果您为平台启用正确的优化应该做的事情)。

这也是不可能的,因为我们现在受到缓存带宽的限制,而缓存带宽并没有更快地增长。我认为这是指向我们的一个非常重要的事实,因为如果您受内存限制并寻求更快的解决方案,那么您将需要很长时间。

答案 14 :(得分:0)

这是memcpy的替代C版本,它是可内联的,我发现在我使用的应用程序中,它的性能优于用于Arm64的GCC的memcpy约50%。它是独立于64位平台的。如果使用实例不需要一点速度,则可以删除尾部处理。复制uint32_t数组,较小的数据类型未经测试,但可能有效。可能能够适应其他数据类型。 64位复制(同时复制两个索引)。 32位也应该可以,但是速度较慢。感谢Neoscrypt项目。

    static inline void newmemcpy(void *__restrict__ dstp, 
                  void *__restrict__ srcp, uint len)
        {
            ulong *dst = (ulong *) dstp;
            ulong *src = (ulong *) srcp;
            uint i, tail;

            for(i = 0; i < (len / sizeof(ulong)); i++)
                *dst++ = *src++;
            /*
              Remove below if your application does not need it.
              If console application, you can uncomment the printf to test
              whether tail processing is being used.
            */
            tail = len & (sizeof(ulong) - 1);
            if(tail) {
                //printf("tailused\n");
                uchar *dstb = (uchar *) dstp;
                uchar *srcb = (uchar *) srcp;

                for(i = len - tail; i < len; i++)
                    dstb[i] = srcb[i];
            }
        }

答案 15 :(得分:0)

CPU的命令集通常支持

内存到内存,memcpy通常会使用它。这通常是最快的方式。

您应该检查您的CPU究竟在做什么。在Linux上,使用sar -B 1或vmstat 1或通过查看/ proc / memstat来监视swapi的输入和输出以及虚拟内存的有效性。您可能会发现您的副本必须将大量页面推出以释放空间,或者将其读入等等。

这意味着您的问题不在于您用于复制的内容,而是您的系统如何使用内存。您可能需要减少文件缓存或开始先写出来,或将页面锁定在内存中等等。

答案 16 :(得分:0)

nos是对的,你说的太多了。

要查看调用它的位置和原因,只需在调试器下暂停一下,然后查看堆栈。

答案 17 :(得分:0)

我认为如果memcpy的性能对你来说已经成为一个问题,你必须拥有大量的内存区域吗?

在这种情况下,我同意nos的建议,找出一些不复制东西的方法..

您可能应该尝试使用其他数据结构,而不是在需要更改时复制大量内存。

在没有真正了解您的问题区域的情况下,我建议您仔细查看persistent data structures并执行您自己的一个或重复使用现有的实施。