非交換運算元的單經線並行約簡

有時,減少必須在非常小的範圍內執行,作為更大的 CUDA 核心的一部分。例如,假設輸入資料恰好有 32 個元素 - warp 中的執行緒數。在這種情況下,可以分配單個扭曲來執行減少。鑑於 warp 在完美同步中執行,與塊級減少相比,可以刪除許多 __syncthreads() 指令。

static const int warpSize = 32;

__device__ int sumNoncommSingleWarp(volatile int* shArr) {
    int idx = threadIdx.x % warpSize; //the lane index in the warp
    if (idx%2 == 0) shArr[idx] += shArr[idx+1];
    if (idx%4 == 0) shArr[idx] += shArr[idx+2];
    if (idx%8 == 0) shArr[idx] += shArr[idx+4];
    if (idx%16 == 0) shArr[idx] += shArr[idx+8];
    if (idx == 0) shArr[idx] += shArr[idx+16];
    return shArr[0];
}

shArr 最好是共享儲存器中的陣列。warp 中的所有執行緒的值應該相同。如果 sumCommSingleWarp 被多個 warp 呼叫,則 shArr 應該在 warp 之間不同(每個 warp 中相同)。

引數 shArr 被標記為 volatile,以確保在指示的位置實際執行對陣列的操作。否則,對 shArr[idx] 的重複分配可以優化為對暫存器的分配,只有最終分配是實際儲存到 shArr。當發生這種情況時,其他執行緒看不到即時分配,從而產生不正確的結果。注意,你可以將普通的非易失性陣列作為 volatile 引數傳遞,與將非 const 作為 const 引數傳遞時相同。

如果一個人不關心 shArr[1..31] 的最終內容並且可以用零填充 shArr[32..47],可以簡化上面的程式碼:

static const int warpSize = 32;

__device__ int sumNoncommSingleWarpPadded(volatile int* shArr) {
    //shArr[32..47] == 0
    int idx = threadIdx.x % warpSize; //the lane index in the warp
    shArr[idx] += shArr[idx+1];
    shArr[idx] += shArr[idx+2];
    shArr[idx] += shArr[idx+4];
    shArr[idx] += shArr[idx+8];
    shArr[idx] += shArr[idx+16];
    return shArr[0];
}

在此設定中,我們刪除了所有 if 條件,這些條件構成了大約一半的指令。額外的執行緒執行一些不必要的新增,將結果儲存到 shArr 的單元格中,最終對最終結果沒有影響。由於 warp 在 SIMD 模式下執行,我們實際上並沒有通過讓這些執行緒無所事事來節省時間。