[英]Parallel binomial coefficients using SIMD instructions
背景
我最近一直在使用一些舊代碼(~1998)並重寫其中一些以提高性能。 以前在 state 的基本數據結構中,我將元素存儲在幾個 arrays 中,現在我使用的是原始位(對於需要少於 64 位的情況)。 也就是說,在我有一個b
元素數組之前,現在我在單個 64 位 integer 中設置了b
位,指示該值是否是我的 state 的一部分。
使用像_pext_u64
和_pdep_u64
這樣的內在函數,我設法讓所有操作的速度提高了 5-10 倍。 我正在處理最后一個操作,這與計算完美的 hash function 有關。
hash function 的確切細節並不太重要,但歸結為計算二項式系數( n choose k
- n!/((nk)!k!)
對於各種n
和k
。我當前的代碼使用大量查找表,這可能很難單獨加速(除了我沒有測量的表中可能的緩存未命中)。
但是,我在想,使用 SIMD 指令,我可能能夠直接並行計算多個狀態的這些指令,從而提高整體性能。
一些限制:
b
位(代表小數)。k
值與b
有關,在計算中變化均勻。 這些值很小(大部分時間 <= 5)。因此,我可以相當容易地寫出並行執行此操作的數學運算,並將所有操作保持為 integer 無余數乘法/除法,同時保持在 32 位以內。 整體流程為:
n choose k
計算。但是,我之前沒有編寫過 SIMD 代碼,所以我仍在了解所有可用功能及其注意事項/效率。
例子:
以前我會把我的數據放在一個數組中,假設總是有 5 個元素:
[3 7 19 31 38]
現在我為此使用一個 64 位值:
0x880080088
這使得許多其他操作非常有效。 對於完美的 hash 我需要有效地計算這樣的東西(使用c
供選擇):
(50c5)-(38c5) + (37c4)-(31c4) + (30c3)-(19c3) +...
但是,在實踐中,我有一堆這些要計算,只是值略有不同:
(50c5)-(Xc5) + ((X-1)c4)-(Yc4) + ((Y-1)c3)-(Zc3) +...
所有 X/Y/Z... 都會有所不同,但每個的計算形式都是相同的。
問題:
我對通過轉換為 SIMD 操作來提高效率的直覺是否合理? (一些消息來源建議“不” ,但這是計算單個系數的問題,而不是並行計算多個系數。)
有沒有比重復的_tzcnt_u64
調用更有效的方法來將位提取到 SIMD 操作的數據結構中? (例如,如果有幫助,我可以暫時將我的 64 位 state 表示分解為 32 位塊,但我不能保證在每個元素中設置相同數量的位。)
當我知道不會溢出時,計算二項式系數的幾個順序乘法/除法運算的最佳內在函數是什么。 (當我查看英特爾參考資料時,我在瀏覽所有變體時無法快速解釋命名 - 不清楚我想要的東西是否可用。)
如果直接計算系數不太可能有效,SIMD 指令可以用於並行查找我之前的系數查找表嗎?
(我很抱歉將幾個問題放在一起,但考慮到具體情況,我認為將它們放在一起會更好。)
這是一種可能的解決方案,它一次使用一個 state 從查找表進行計算。 在多個狀態下並行執行此操作可能比使用單個 state 更有效。 注意:對於獲得 6 個元素組合的固定情況,這是硬編碼的。
int64_t GetPerfectHash2(State &s)
{
// 6 values will be used
__m256i offsetsm1 = _mm256_setr_epi32(6*boardSize-1,5*boardSize-1,
4*boardSize-1,3*boardSize-1,
2*boardSize-1,1*boardSize-1,0,0);
__m256i offsetsm2 = _mm256_setr_epi32(6*boardSize-2,5*boardSize-2,
4*boardSize-2,3*boardSize-2,
2*boardSize-2,1*boardSize-2,0,0);
int32_t index[9];
uint64_t value = _pext_u64(s.index2, ~s.index1);
index[0] = boardSize-numItemsSet+1;
for (int x = 1; x < 7; x++)
{
index[x] = boardSize-numItemsSet-_tzcnt_u64(value);
value = _blsr_u64(value);
}
index[8] = index[7] = 0;
// Load values and get index in table
__m256i firstLookup = _mm256_add_epi32(_mm256_loadu_si256((const __m256i*)&index[0]), offsetsm2);
__m256i secondLookup = _mm256_add_epi32(_mm256_loadu_si256((const __m256i*)&index[1]), offsetsm1);
// Lookup in table
__m256i values1 = _mm256_i32gather_epi32(combinations, firstLookup, 4);
__m256i values2 = _mm256_i32gather_epi32(combinations, secondLookup, 4);
// Subtract the terms
__m256i finalValues = _mm256_sub_epi32(values1, values2);
_mm256_storeu_si256((__m256i*)index, finalValues);
// Extract out final sum
int64_t result = 0;
for (int x = 0; x < 6; x++)
{
result += index[x];
}
return result;
}
請注意,我實際上有兩個類似的案例。 在第一種情況下,我不需要_pext_u64
,並且此代碼比我現有的代碼慢約 3 倍。 在第二種情況下,我需要它,它快 25%。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.