簡體   English   中英

優化索引數組求和

[英]Optimize indexed array summation

我有以下C ++代碼:

const int N = 1000000
int id[N]; //Value can range from 0 to 9
float value[N];

// load id and value from an external source... 

int size[10] = { 0 };
float sum[10] = { 0 };
for (int i = 0; i < N; ++i)
{
    ++size[id[i]];
    sum[id[i]] += value[i];
}

我應該如何優化循環?

我考慮過使用SSE將每4個浮點加到一個總和,然后經過N次迭代,總和就是xmm寄存器中4個浮點的總和,但是當對源進行這樣的索引並需要寫出時這是行不通的到10個不同的數組。

使用SIMD指令很難優化這種循環。 在大多數SIMD指令集中,不僅沒有一種簡便的方法來進行這種索引讀取(“聚集”)或寫入(“散布”),即使存在,該特定循環仍然存在您可能遇到的問題兩個值映射到一個SIMD寄存器中的相同id ,例如

id[0] == 0
id[1] == 1
id[2] == 2
id[3] == 0

在這種情況下,顯而易見的方法(此處為偽代碼)

x = gather(size, id[i]);
y = gather(sum, id[i]);
x += 1; // componentwise
y += value[i];
scatter(x, size, id[i]);
scatter(y, sum, id[i]);

也不會工作!

僅通過蠻力比較就可以確定是否存在少量的可能情況(例如,假設sumsize只有3個元素),但這並不能真正擴展。

一種無需使用SIMD即可更快地實現此目的的方法是通過使用展開來稍微破壞指令之間的依賴關系:

int size[10] = { 0 }, size2[10] = { 0 };
int sum[10] = { 0 }, sum2[10] = { 0 };
for (int i = 0; i < N/2; i++) {
  int id0 = id[i*2+0], id1 = id[i*2+1];
  ++size[id0];
  ++size2[id1];
  sum[id0] += value[i*2+0];
  sum2[id1] += value[i*2+1];
}

// if N was odd, process last element
if (N & 1) {
  ++size[id[N]];
  sum[id[N]] += value[N];
}

// add partial sums together
for (int i = 0; i < 10; i++) {
  size[i] += size2[i];
  sum[i] += sum2[i];
}

不過,這是否有幫助取決於目標CPU。

好吧,您在循環中兩次調用了id [i]。 您可以將其存儲在變量中,也可以將其存儲在寄存器int中。

register int index;
for(int i = 0; i < N; ++i)
{
index = id[i];
++size[index];
sum[index] += value[i];
}

MSDN文檔說明了有關注冊的信息:

register關鍵字指定將變量存儲在計算機寄存器中。

編譯器不接受用戶對寄存器變量的請求。 相反,在啟用全局寄存器分配優化(/ Oe選項)時,它會自行選擇寄存器。 但是,將保留與register關鍵字關聯的所有其他語義。

您可以做的是使用-S標志(如果不使用gcc則進行編譯)進行編譯,並使用-O-O2-O3標志比較各種匯編輸出。 優化循環的一種常用方法是進行一定程度的展開,例如(一個非常簡單,幼稚的示例):

int end = N/2;
int index = 0;
for (int i = 0; i < end; ++i)
{
    index = 2 * i;
    ++size[id[index]];
    sum[id[index]] += value[index];
    index++;
    ++size[id[index]];
    sum[id[index]] += value[index];
}

這會將cmp指令的數量減少一半。 但是,任何半適當的優化編譯器都可以為您完成此任務。

您確定會有所不同嗎? 可能是,“從外部來源獲取ID”的加載要比加起來的值花費更長的時間。

在知道瓶頸所在之前,不要進行優化。

編輯以回應評論 :您誤解了我。 如果從硬盤加載ID需要10秒鍾,那么處理列表所花費的幾分之一秒在更宏大的方案中就無關緊要。 假設加載需要10秒,處理需要1秒:

您優化處理循環,使其花費0秒(幾乎不可能,但這只能說明一點),然后仍然需要10秒。 11秒實際上並不會影響性能,您最好將優化時間集中在實際的數據負載上,因為這很可能是最慢的部分。

實際上,進行雙緩沖數據加載可能是最佳選擇。 即,您加載緩沖區0,然后開始加載緩沖區1。在緩沖區1加載過程中,您將處理緩沖區0。完成后,在處理緩沖區1時開始加載下一個緩沖區,依此類推。 這樣,您可以完全攤銷處理成本。

進一步的編輯 :實際上,最佳的優化可能是將東西加載到一組存儲桶中,從而消除了te計算的“ id [i]”部分。 然后,您可以簡單地分流到3個線程,每個線程都使用SSE添加。 這樣,您可以使它們同時運行,並且如果您至少擁有三核計算機,則可以在十分之一的時間內處理整個數據。 組織數據進行最佳處理將始終為IMO帶來最佳優化。

根據目標計算機和編譯器的不同,請查看是否具有_mm_prefetch內部函數並進行嘗試。 早在Pentium D時代,只要您在需要數據之前就預取了幾次循環迭代,就可以使用asm指令對該內在函數進行預取是真正的速度勝利。

有關英特爾的更多信息,請參見此處 (PDF中的第95頁)。

這種計算是微不足道的可並行化的。 只需添加

#pragma omp parallel_for縮減(+:size,+:sum)計划(靜態)

如果您具有OpenMP支持(在GCC中為-fopenmp),則在循環的正上方。但是,我並不希望在典型的多核台式機上有太多的加速。 您對獲取的每個項目所做的計算很少,幾乎可以肯定會受到內存帶寬的限制。

如果您需要對給定的id映射執行多次求和(即value []數組的更改次數比id []頻繁),則可以通過將value []元素按id順序預先排序來將內存帶寬需求減半從id []中消除按元素提取:

對於(i = 0,j = 0,k = 0; j <10; sum [j] + = tmp,j ++)

對於(k + = size [j],tmp = 0; i <k; i ++)

  tmp += value[i];

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM