2010-06-03 102 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(); 
    } 
}