更快的替代memcpy?

Ton*_*ark 37 c performance memcpy

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

nos*_*nos 119

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

  • 关于更快的memcpy这是一个很好的问题,但这个答案提供了一种解决方法,而不是答案.例如http://software.intel.com/en-us/articles/memcpy-performance/解释了为什么memcpy通常效率低得多的一些非常严重的原因. (9认同)
  • 关于memcpy的英特尔帖子的早期链接似乎不再公开,但是文章可以在这里找到(http://web.archive.org/web/20131223174037/http://software.intel.com/en-us/文章/ memcpy-performance /)和[here](http://codepen.io/anon/pen/WvQyRd?editors=100). (6认同)
  • 这个答案没有回答问题。这个问题是一个有效的问题。我会要求堆栈溢出删除“已回答”标志。 (3认同)
  • +1,我们最近遇到一个问题,当我们的一些代码突然显着减慢并在处理某个文件时消耗了大量额外内存.原来这个文件有一些巨大的元数据块,而其他苍蝇没有元数据或小块.这些元数据被复制,复制,复制,消耗时间和内存.用pass-by-const-reference替换复制. (2认同)
  • 即使在今天,这也远远不正确。memcpy 通常很幼稚 - 当然不是复制内存的最慢方法,但通常很容易通过一些循环展开来击败,并且您可以使用汇编器走得更远。 (2认同)

Ser*_*tch 22

这是带有AVX2指令集的x86_64的答案.虽然类似的东西可能适用于带有SIMD的ARM/AArch64.

在完全填充单个内存通道的Ryzen 1800X上(2个插槽,每个16 GB DDR4),以下代码比memcpy()MSVC++ 2017编译器快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,最后一个字节.同样在中间,__m256i可以使用2-3个值的本地数组作为来自源的对齐读取和对目标的对齐写入之间的代理.

#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();
}
Run Code Online (Sandbox Code Playgroud)

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

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

memcpy(): 17 208 004 271 bytes/sec.
Stream copy: 26 842 874 528 bytes/sec.
Run Code Online (Sandbox Code Playgroud)

请注意,在这些测量中,输入和输出缓冲区的总大小除以经过的秒数.因为对于数组的每个字节,有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
Run Code Online (Sandbox Code Playgroud)

代码是:

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();
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 一个好的 memcpy(如 GNU/Linux 上的 glibc)将使用超过特定大小阈值的 NT 存储,或者在某些 CPU 上简单地使用“rep movsb”。如果您的 C 实现的 memcpy 尚未执行此操作,或者您知道此副本应该是非临时的,那么手动执行此操作可能是有意义的。 (2认同)

INS*_*INS 11

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

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

  • 对于ARM,libc impl现在比你自己创建的更快.对于小型副本(任何少于页面的内容),在函数内部使用ASM循环会更快.但是,对于大型副本,您将无法击败libc impl,因为diff处理器具有略微不同的"最佳"代码路径.例如,Cortex8最适合使用NEON复制指令,但使用ldm/stm ARM指令时,Cortex9速度更快.你不能编写一个对两个处理器来说都很快的代码,但是你可以为大缓冲区调用memcpy. (2认同)

sha*_*oth 6

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


小智 6

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

非常快速的图像处理memcpy?.

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

  • 尽管 `memmove` 必须检查和处理重叠,`memcpy` 不需要这样做。更大的问题是,为了在复制大块时高效,`memcpy` 的实现需要在开始工作之前选择一种复制方法。如果代码需要能够复制任意数量的字节,但该数字在 90% 的时间内是一个,9% 的时间是两个,0.9% 的时间是三个,等等,以及 `count` 的值, `dest` 和 `src` 之后将不再需要,然后一个内联的 `if (count) do *dest+=*src; while(--count &gt; 0);` 可能比“更智能”的例程更好。 (2认同)