[英]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 中存儲為大端時,它將以正確的順序包含預期的字節
有關該算法的更多詳細信息,請參閱如何從 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.