簡體   English   中英

將 5 位位域從 u32 擴展到 u8[6] 緩沖區,這是最有效的方法

[英]Expanding 5-bit bitfields from a u32 into a u8[6] buffer, the most efficient way possible

這是一個優化問題。 我想將六個 5 位元素的位域復制到 u8 緩沖區,天真地這樣做:

void Expand(u32 x, u8 b[6]) {
    b[0] = (x >> 0) & 31;
    b[1] = (x >> 5) & 31;
    b[2] = (x >> 10) & 31;
    b[3] = (x >> 15) & 31;
    b[4] = (x >> 20) & 31;
    b[5] = (x >> 25) & 31;
}

這是由x86 msvc v19.latest 、 flags /O2 /Ot /Gr 、 gcc 和 clang 生成的程序集將給出大致相同的結果。

@Expand@8 PROC
        mov     al, cl
        and     al, 31
        mov     BYTE PTR [edx], al
        mov     eax, ecx
        shr     eax, 5
        and     al, 31
        mov     BYTE PTR [edx+1], al
        mov     eax, ecx
        shr     eax, 10
        and     al, 31
        mov     BYTE PTR [edx+2], al
        mov     eax, ecx
        shr     eax, 15
        and     al, 31
        mov     BYTE PTR [edx+3], al
        mov     eax, ecx
        shr     eax, 20
        shr     ecx, 25
        and     al, 31
        and     cl, 31
        mov     BYTE PTR [edx+4], al
        mov     BYTE PTR [edx+5], cl
        ret     0
@Expand@8 ENDP

但我就是不喜歡; 我知道它確實做了它應該做的事情,在我看來它可能會更有效率。
對我來說,它看起來像是一個 30 位數字,需要在插入零時放大到 48 位數字。

                  11111 11111 11111 11111 11111 11111
                                                    ↓
00011111 00011111 00011111 00011111 00011111 00011111

我一直在嘗試 SHIFTing、ORing,最后只使用 u64 ( 0x1f1f1f1f1f1f ) 進行 ANDing,但我的優化工作仍然沒有成功。 我相信這應該可以在不到 10 條指令內完成,任何指導都將不勝感激。

編輯
我又撓了撓頭,這是迄今為止我能想到的最好的:

void Expand(u32 x, u8 b[6]) {
    memset(b, 31, 6);
    b[0] &= x;
    b[1] &= x >>= 5;
    b[2] &= x >>= 5;
    b[3] &= x >>= 5;
    b[4] &= x >>= 5;
    b[5] &= x >>= 5;
}

編譯為:

@Expand@8 PROC
        mov     eax, 0x1f1f1f1f
        mov     DWORD PTR [edx], eax
        mov     WORD PTR [edx+4], ax
        and     BYTE PTR [edx], cl
        shr     ecx, 5
        and     BYTE PTR [edx+1], cl
        shr     ecx, 5
        and     BYTE PTR [edx+2], cl
        shr     ecx, 5
        and     BYTE PTR [edx+3], cl
        shr     ecx, 5
        and     BYTE PTR [edx+4], cl
        shr     ecx, 5
        and     BYTE PTR [edx+5], cl
        ret     0
@Expand@8 ENDP

這是一個跨平台的解決方案,只需要一個幾乎所有桌面架構都可用的快速乘法器

void Expand(uint32_t x, uint8_t b[6]) {
    uint32_t x024 = x & 0b00'00000'11111'00000'11111'00000'11111;
    uint32_t x135 = x & 0b00'11111'00000'11111'00000'11111'00000;
    uint64_t r024 = x024 * 0x0100'0000'4000'0010ULL & 0x1F001F001F000000;
    uint64_t r135 = x135 * 0x0040'0000'1000'0004ULL & 0x001F001F001F0000;
    uint64_t result = r024 | (r135 >> 11);
#if !BIG_ENDIAN
    result = htonll(result);
#endif
    memcpy(b, &result, 6);
}

有關詳細的數學運算,請參見下文。 它需要大約 8-9 次操作並在 2 個並行鏈中運行。 您可以通過傳遞 8 字節數組而不是 6 並在必要時稍后恢復最后 2 個元素b[6] / b[7]來改進它。

但是您應該真正使用#ifdef並為每個受支持的平台提供有效的實現,並為其他平台提供類似上述的后備通用解決方案。 x86 上最快的方法是 SIMD 或 PDEP,具體取決於您是為大型陣列執行此操作還是偶爾執行此操作。 所有其他平台也有自己的 SIMD 可用於加速。 或者,您也可以使用與平台無關的 SIMD 庫來自動為任何架構發出高效的 SIMD 代碼。


請注意,指令數不是衡量性能的標准 並非所有指令都是平等的。 您的“最佳”實際上比第一個版本更糟糕,因為它具有很長的依賴鏈,而 CPU 可以同時啟動 5 個獨立執行並與后者並行運行

請記住,許多指令很慢,因此多個更簡單的等效指令會更快。 可以並行執行的多條指令也將比具有依賴性的較短序列更快。 而且短循環也比直接運行更糟糕


算法背后的數學

讓輸入為00aaaaabbbbbcccccdddddeeeeefffff的 32 位。 掩碼后,乘法將產生正確 position 中的位

                                  0000000bbbbb00000ddddd00000fffff (x024)
× 0000000100000000000000000000000001000000000000000000000000010000 (0x0100'0000'4000'0010)
  ────────────────────────────────────────────────────────────────
                              0000000bbbbb00000ddddd00000fffff
    0000000bbbbb00000ddddd00000fffff
+ 000fffff
  0000000100000000000000000000000001000000000000000000000000010000
  ────────────────────────────────────────────────────────────────
& 0001111100000000000111110000000000011111000000000000000000000000 (0x1F001F001F000000)
  ────────────────────────────────────────────────────────────────
= 000fffff00000000000ddddd00000000000bbbbb000000000000000000000000
                                  00aaaaa00000ccccc00000eeeee00000 (x135)
× 0000000001000000000000000000000000010000000000000000000000000100 (0x0040'0000'1000'0004)
  ────────────────────────────────────────────────────────────────
                                00aaaaa00000ccccc00000eeeee00000
+     00aaaaa00000ccccc00000eeeee00000
  eeeee00000
  ────────────────────────────────────────────────────────────────
& 11111000000000001111100000000000111110000000000000000            (0x001F001F001F0000)
  ────────────────────────────────────────────────────────────────
= eeeee00000000000ccccc00000000000aaaaa000000000000000000000000000

合並上述兩個結果,我們得到000fffff000eeeee000ddddd000ccccc000bbbbb000aaaaa0000000000000000 ,當在 memory 中存儲為大端時,它將以正確的順序包含預期的字節

用於比較的 Output 組件

有關該算法的更多詳細信息,請參閱如何從 8 個布爾值中創建一個字節(反之亦然)?

您的版本有 6 個班次(實際上是 5 個)和 6 個掩碼(當然還有 6 個作業。)

我在評論中建議“分而治之”。

這個版本有4個班次和4個面具。 它可能會被整理,我不知道它的裝配是什么樣的。 看到會很有趣!

反正...

void expand( uint32_t x, uint8_t b[ 6 ] ) {
    union {
        uint32_t val32;
        struct {
            uint16_t lo;
            uint16_t hi;
        } b15;
    } v, valt;

    v.val32 = x << 1; // to shove b15 into b16
    v.b15.lo = (uint16_t)(x & 0xFFFF);

    valt.val32 = (v.val32 >> 0) & 0x001F001F;
    b[0] = (uint8_t)valt.b15.lo;
    b[3] = (uint8_t)valt.b15.hi;

    valt.val32 = (v.val32 >> 5) & 0x001F001F;
    b[1] = (uint8_t)valt.b15.lo;
    b[4] = (uint8_t)valt.b15.hi;

    valt.val32 = (v.val32 >>10) & 0x001F001F;
    b[2] = (uint8_t)valt.b15.lo;
    b[5] = (uint8_t)valt.b15.hi;
}

int main() {
    uint8_t b[ 6 ] = { 7, 24, 31, 0, 6, 1, }; // 0 - 31 only!!

    uint32_t x = (b[5]<<25) | (b[4]<<20) | (b[3]<<15) | (b[2]<<10) | (b[1]<<5) | (b[0]<<0);

    memset( b, 0, sizeof b );

    expand( x, b );

    for( int i = 0; i < 6; i++ )
        printf( "[%d] %u  ", i, b[i] );
    puts( "" );

    return 0;
}

Output

[0] 7  [1] 24  [2] 31  [3] 0  [4] 6  [5] 1

對我來說,它看起來像是一個 30 位數字,需要在插入零時放大到 48 位數字。

我明白你為什么這么說,但既然你也說你想成為建築不可知論者,那就不太對了。 您的位域表示和字節數組表示之間有一個重要但有些微妙的區別:壓縮數字中的位域標識/索引是 function 的,而數組中的字節標識/索引是 function 的存儲順序 因此,更接近於說您想要將 30 位本機格式數字轉換為 48 位小端數字。

實現這一點的一種方法是簡單地將數字讀出到目標數組中,這正是問題中提出的兩個替代方案所做的。 從這個意義上說,你已經在做 但是,如果您將算術上的數字擴展為一個單獨的步驟,那么您必須承認您之后需要將其存儲在數組中。 我想您已經考慮將memcpy()就位,但請注意,要使該算術 + memcpy()作為直接讀出的替代方案有意義,該算術必須是拱敏感的。

但無論如何,讓我們來探討一下算術。 畢竟,也許你不關心非小端架構。 鑒於您的評論...

理想情況下,我希望這只是“在每個拱門上編譯良好的 C 代碼”,所以沒有奇怪的指令或隱藏的內在函數

...,我將只考慮標准 C 提供的操作。 目標是計算一個 64 位 integer,在其最低有效 48 位中包含所需的擴展。

這里的一個關鍵限制是輸入的每個位域都必須移動不同的距離。 最直接的方法是逐個字段進行,可能是這樣的:

    uint64_t expanded =
        ( (uint64_t) x)       &           0x1f) +
        (((uint64_t) x << 3)  &         0x1f00) +
        (((uint64_t) x << 6)  &       0x1f0000) +
        (((uint64_t) x << 9)  &     0x1f000000) +
        (((uint64_t) x << 12) &   0x1f00000000) +
        (((uint64_t) x << 15) & 0x1f0000000000);

或者編譯器可能會更友好地對待這種變化:

    uint64_t temp = x;
    uint64_t expanded = temp &   0x1f;
    temp <<= 3;
    expanded |= temp &         0x1f00;
    temp <<= 3;
    expanded |= temp &       0x1f0000;
    temp <<= 3;
    expanded |= temp &     0x1f000000;
    temp <<= 3;
    expanded |= temp &   0x1f00000000;
    temp <<= 3;
    expanded |= temp & 0x1f0000000000;

但是,當然,它們都比您的替代方案執行更多的算術運算,因此沒有理由期望更簡單的匯編代碼。 盡管如此,您可能會看到由於較少訪問 memory 以存儲結果(假設使用memcpy() ;未顯示,假設已優化以避免實際的 function 調用),從而提高了整體性能。

也許您正在尋找一個有點小技巧的技巧,以從更少的算術運算中獲得更多的工作。 scope 很小,因為首先您只有六個字段可供操作。 除 6 (x 1) 之外的唯一方法是 3 x 2,對於后者,您最多只能期望 3 + 2 - 1 = 4 組操作而不是 6 組。 像這樣的東西:

uint64_t temp = (((uint64_t) x << 9) & 0xffffff000000) | (x & 0x7fff);
uint64_t expanded = temp &     0x1f00001f;
temp <<= 3;
expanded |=         temp &   0x1f00001f00;
temp <<= 3;
expanded |=         temp & 0x1f00001f0000;

相對於簡單的版本,這在算術運算計數方面產生了適度的增益:三班而不是五班,五班而不是六班,三班而不是五班。 編譯器可能會也可能不會像對待其他代碼一樣善意地對待這段代碼。 它仍然比您的直接讀出替代方案更多的算術運算。

暫無
暫無

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

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