2010-06-03 1011 views
34

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

+1

简短的回答:也许,这是可能的。提供更多的细节,如架构,平台和其他。在嵌入式世界中,很可能会重写libc中一些功能不佳的功能。 – INS 2010-06-03 08:55:58

回答

111

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

+2

+1,我们最近有一个问题,当我们的一些代码突然减慢并且在处理某个文件时消耗了大量额外的内存。原来文件有一些巨大的元数据块,而其他苍蝇没有元数据或小块。这些元数据被复制,复制,复制,同时消耗时间和内存。用传递const引用替换复制。 – sharptooth 2010-06-03 07:36:47

+6

这是一个关于更快速的memcpy的好问题,但是这个答案提供了一个解决方法,而不是一个答案。例如。 http://software.intel.com/en-us/articles/memcpy-performance/解释了一些非常严重的原因,为什么memcpy通常效率低得多。 – 2012-01-23 14:26:38

+0

是否有可能使用写入时复制技术,无论是在低层还是在代码中?你会需要内存块的大小相似的整数倍页面?然后,您只需将指向现实生活的指针留给同一内存,并让内存管理器在数据更改时根据需要制作页面的副本。 – 2013-10-24 09:29:01

6

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

3

通常情况下不会复制一个副本。你是否可以调整你的功能不复制我不知道,但它值得期待。

3

有时候喜欢的memcpy,memset的功能,...两种不同的方式来实现:

  • 一次作为一个真正的功能
  • 曾经因为一些组件会立即联

并非所有编译器默认采用内联程序集版本,您的编译器可能会默认使用函数变体,由于函数调用而导致一些开销。 检查您的编译器以了解如何使用函数的内在变体(命令行选项,编译指示,...)。

编辑:请参阅http://msdn.microsoft.com/en-us/library/tzkfha43%28VS.80%29.aspx以了解Microsoft C编译器上的内在函数的解释。

0

如果memcpy的性能已成为您的问题,那么我认为您必须拥有大量您想要复制的内存区域?

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

而是有一个巨大的存储斑点每当你需要改变它围绕复制,你应该尝试一些替代的数据结构。

没有真正了解您的问题区域的任何信息,我会建议您仔细看看persistent data structures并实施您自己的或重新使用现有的实施。

2

检查编译器/平台手册。对于一些使用memcpy的微处理器和DSP工具包,要比intrinsic functionsDMA操作要慢得多。

2

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

10

请给我们提供更多的细节。在i386体系结构中,memcpy很可能是最快的复制方式。但是在编译器没有优化版本的不同架构上,最好重写memcpy函数。我在使用汇编语言的定制ARM架构上做了这个。如果你传输大块内存,那么DMA可能是你正在寻找的答案。

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

+1

对于ARM,libc impl现在更快,您将能够创建自己的内容。对于小副本(小于页面的任何内容),在函数内部使用ASM循环可能会更快。但是,对于大型副本,您将无法击败libc impl,因为diff处理器具有稍微不同的“最佳”代码路径。例如,Cortex8在NEON拷贝指令上效果最好,但是使用ldm/stm ARM指令时Cortex9速度更快。您无法编写对于两个处理器来说都很快的代码片段,但只需调用memcpy即可访问大型缓冲区。 – MoDJ 2013-07-04 19:56:21

+0

@MoDJ:我希望标准C库包含几个不同的memcpy变体,它们通常具有相同的语义,在全部产生定义的行为的情况下,但是具有不同的优化情况,以及在某些情况下 - 对齐对比对齐使用的限制。如果代码通常需要复制少量的字节或已知对齐的字,那么一个天真的字符一次实现可以在更短的时间内完成这项工作,而不是某些fancier memcpy()实现需要决定的时间一个行动的过程。 – supercat 2014-07-26 02:20:13

0

号是正确的,你打电话太多。

要查看你从哪里调用它以及为什么,只需在调试器下暂停几次并查看堆栈。

0

存储器到存储器通常在CPU的指令集的支持,和memcpy通常会利用这一点。这通常是最快的方法。

你应该检查一下你的CPU在做什么。在Linux上,使用sar -B 1或vmstat 1或通过查看/ proc/memstat来监视swapi输入输出和虚拟内存效率。您可能会看到您的副本有大量的页面推送到自由空间,或阅读他们在等

这将意味着你的问题不在于你用什么副本,但是如何你的系统使用记忆。您可能需要降低文件高速缓存或更早开始写出来,或在内存中锁定页等

6

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

Very fast memcpy for image processing?

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

+0

我用sse2试过类似这样的代码。结果发现它比我的amd系统慢了4倍。如果你能帮到你,最好不要复制。 – Matt 2013-04-03 21:31:28

+0

虽然'memmove'必须检查并处理重叠,'memcpy'不需要这样做。更大的问题是,为了在复制大块时高效,'memcpy'的实现需要在可以开始工作之前选择复制方法。如果代码需要能够复制任意数量的字节,但是这个数字将是90%的时间,两个9%的时间,三个0.9%的时间等等,以及'count'的值,之后不需要'dest'和'src',然后是一个内联的if(count)do * dest + = * src; while( - count> 0);'可以比“更聪明”的例程更好。 – supercat 2015-03-16 17:41:23

+0

顺便说一句,在某些嵌入式系统中,memcpy可能不是最快的方法的另一个原因是,DMA控制器有时可以用比CPU更少的开销复制一块内存,但是最有效的方式来执行复制可能是启动DMA,然后在DMA正在运行时执行其他处理。在具有独立前端代码和数据总线的系统上,可以配置DMA,以便在CPU不需要其他任何数据总线时在每个周期复制数据。使用... – supercat 2015-04-05 19:18:56

1

您应该检查为您的代码生成的汇编代码。什么,你不想要的是有memcpy调用生成标准库到memcpy函数的调用 - 你想要的是拥有最好的ASM指令复制数据量最大反复呼吁 - 像rep movsq

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

所以,你想要么能告诉memcpy有什么用常量表达式数据的大小,编译器可以优化的内容。这样就不会对memcpy进行呼叫。你不想要的是传递给只有在运行时才知道的变量memcpy。这转换成函数调用和大量测试来检查最佳的复制指令。有时,出于这个原因,简单的for循环比memcpy更好(消除一个函数调用)。而你真的真的不想要传递给memcpy需要复制的奇数个字节。

6

这是带有AVX2指令集的x86_64的答案。虽然类似的东西可能适用于带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-only三/四通道系统,预计速度不会更快,因为要将它们全部加载,您需要一次加载/存储超过32个字节(三字节为48字节,四通道为64字节系统),而AVX2可以一次加载/存储不超过32个字节。尽管在没有AVX512甚至AVX2的情况下,某些系统上的多线程可以缓解这种情况。

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

对于非多尺寸和非对齐的块,可以写成序列/结尾代码,将块头部和尾部的宽度一次减至16(SSE4.1),8,4,2以及最后1个字节。在中间也可以使用本地数组的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(); 
    } 
}