這個特殊問題是在對 HPC 代碼進行一些分析時出現的。這是一個最小的作業示例。
我有一個源資料向量,我將它復制到一個大的目標向量,然后在另一個地方我將向量復制到一個相同大小的向量。我發現復制到大目標向量需要更長的時間(~2.5X),即使被復制的資料量是相同的。我需要一些幫助來理解行為,以及在復制到大型目標向量時如何提高性能的一些指導。
#include <iostream>
#include <vector>
#include <memory>
#include <chrono>
#include <cstring>
#include <cstdlib>
int main() {
size_t tw = 1024, th=1024, td=10;
std::vector<uint16_t> src_data(tw*th);
for(auto i=0;i<tw*th;i ){src_data[i] = rand()%100;}
std::shared_ptr<std::vector<uint16_t>> large_tile_data_1 =
std::make_shared<std::vector<uint16_t>>(tw*th*td) ;
auto start = std::chrono::high_resolution_clock::now();
for (auto i=0;i<td; i){
auto offset = i*tw*th;
std::memcpy((void *)(&(*(large_tile_data_1->data() offset))),
(void *)(&(src_data[0])), sizeof(uint16_t)*tw*th);
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> elapsed_seconds = end-start;
std::cout<<"elapsed_time " << elapsed_seconds.count() << std::endl;
std::shared_ptr<std::vector<uint16_t>> small_tile_data_2 = std::make_shared<std::vector<uint16_t>>(tw*th) ;
start = std::chrono::high_resolution_clock::now();
for (auto i=0;i<td; i){
auto offset = 0;
std::memcpy((void *)(&(*(small_tile_data_2->data() offset))), (void *)(&(src_data[0])),
sizeof(uint16_t)*tw*th);
}
end = std::chrono::high_resolution_clock::now();
elapsed_seconds = end-start;
std::cout<<"elapsed_time " << elapsed_seconds.count() << std::endl;
}
uj5u.com熱心網友回復:
答案幾乎可以肯定是某種形式的快取。CPU 資料快取、TLB 或記憶體分配行為。
在您的測驗代碼中,第二部分一遍又一遍地覆寫相同的 2 MiB 資料。該大小非常適合大多數 CPU 的 L3 快取。即使沒有,現代 LLC(最后一級快取)也可能足夠聰明,可以切換到適用于這種情況的自適應替換策略。
重復覆寫或其他較小的作業集也將有助于其他方面。例如,它增加了目標記憶體位于 TLB(事務后備緩沖區)中的機會。
另一個方面是,在第一種情況下,您分配了幾兆位元組的記憶體并初始化它們一次,而不是重復。如此大的分配可能由單獨的記憶體映射支持。這些是由作業系統延遲初始化的,并且在首次使用時會產生很高的開銷。在您的第二次較小的使用中,此費用僅在第一個回圈中支付一次。
同樣,在實際程式中,較小的分配更有可能被 malloc 實作快取而不是被釋放到作業系統,這使得重復分配-釋放周期中的重用更快。
uj5u.com熱心網友回復:
由于您要復制相同大小的資料,我會說快取效果不太可能。但是分析您的代碼并檢查快取未命中的數量,以確保這不是您所看到的。
讓我們看一下您減速的另一個候選人:
在大多數 C 庫中,memcpy()都有代碼來處理與某些架構定義的邊界和大尺寸對齊的復制資料以及通用情況的后備。
例如,在 ARM 上,如果記憶體適當對齊,您可以在一條指令中加載或存盤雙字(2 個暫存器)。在 x86 上,您可以利用 SSE 暫存器一次加載 16、32 甚至 64 個位元組,并且您需要正確對齊源和目標以使其快速。
因此,如果您的資料對于源和目標都與 16 位元組對齊,您將獲得一個復制資料的函式,如果不是,您將獲得不同的函式(或者 CPU 執行速度較慢)。malloc()通過or分配的記憶體new通常是 16 位元組對齊的(與任何基本型別的架構最大要求對齊),并且對于memcpy().
但是,如果您隨后將帶有偏移量的資料復制到向量中,您將破壞最佳對齊方式,并且memcpy()由于 CPU 的時間較慢或memcpy()必須使用效率較低的代碼來動態重新對齊資料,因此速度會變慢。
uj5u.com熱心網友回復:
問題是你專注的地方。memcpy與第二個回圈相比,您的代碼在第一個回圈中實際上要慢一些。但是目的地的大小并不是唯一的區別,這兩個回圈做的事情不一樣。如果你讓他們做同樣的事情,無論目的地的大小,他們都會使用相同的時間。
將第一個回圈中的偏移量替換為與第二個回圈相同,然后重試。
我為你試過:
你的版本
Offest = 0 也適用于大向量
這些只是示例,使用比 Godbolt 更可靠的東西來對代碼進行基準測驗。
其他答案和評論已經解釋了可能導致這種行為的原因。優化、快取未命中和其他很酷的詞。
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/481809.html
