簡體   English   中英

使用 SIMD 指令的並行二項式系數

[英]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!)對於各種nk 。我當前的代碼使用大量查找表,這可能很難單獨加速(除了我沒有測量的表中可能的緩存未命中)。

但是,我在想,使用 SIMD 指令,我可能能夠直接並行計算多個狀態的這些指令,從而提高整體性能。

一些限制:

  • 在每個 64 位 state 中總是精確設置b位(代表小數)。
  • 二項式系數中的k值與b有關,在計算中變化均勻。 這些值很小(大部分時間 <= 5)。
  • 最終的 hash 將小於 1500 萬(很容易適合 32 位)。

因此,我可以相當容易地寫出並行執行此操作的數學運算,並將所有操作保持為 integer 無余數乘法/除法,同時保持在 32 位以內。 整體流程為:

  1. 將這些位提取為適合 SIMD 指令的值。
  2. 以避免溢出的方式執行n choose k計算。
  3. 從每個條目中提取出最終的 hash 值

但是,我之前沒有編寫過 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... 都會有所不同,但每個的計算形式都是相同的。

問題:

  1. 我對通過轉換為 SIMD 操作來提高效率的直覺是否合理? 一些消息來源建議“不” ,但這是計算單個系數的問題,而不是並行計算多個系數。)

  2. 有沒有比重復的_tzcnt_u64調用更有效的方法來將位提取到 SIMD 操作的數據結構中? (例如,如果有幫助,我可以暫時將我的 64 位 state 表示分解為 32 位塊,但我不能保證在每個元素中設置相同數量的位。)

  3. 當我知道不會溢出時,計算二項式系數的幾個順序乘法/除法運算的最佳內在函數是什么。 (當我查看英特爾參考資料時,我在瀏覽所有變體時無法快速解釋命名 - 不清楚我想要的東西是否可用。)

  4. 如果直接計算系數不太可能有效,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.

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