[英]Split a number into several numbers, each with only one significant bit
是否有任何有效的算法(或處理器指令)可以幫助將數字(32 位和 64 位)分成幾個數字,其中只有一個 1 位。
我想隔離一個數字中的每個設置位。 例如,
輸入:
01100100
output:
01000000
00100000
00000100
只想到number & mask
。 匯編或С++。
是的,以類似於Brian Kernighan 的算法來計算 set bits ,除了我們提取的位而不是計算位,並在每個中間結果中使用最低的 set 位:
while (number) {
// extract lowest set bit in number
uint64_t m = number & -number;
/// use m
...
// remove lowest set bit from number
number &= number - 1;
}
在現代 x64 匯編中, number & -number
-number 可以編譯為blsi
, number &= number - 1
可以編譯為blsr
,兩者都很快,因此只需要幾個有效的指令即可實現。
由於m
可用,因此可以使用number ^= m
重置最低設置位,但這可能會使編譯器更難看到它可以使用blsr
,這是一個更好的選擇,因為它只直接依賴於number
所以它縮短循環攜帶依賴鏈。
標准方法是
while (num) {
unsigned mask = num ^ (num & (num-1)); // This will have just one bit set
...
num ^= mask;
}
例如從num = 2019
開始,您將按順序排列
1
2
32
64
128
256
512
1024
如果您要一次一個地迭代單個位隔離掩碼,一次生成一個掩碼是有效的; 請參閱@harold 的回答。
但如果你真的只想要所有的掩碼, x86 和 AVX512F 可以有效地並行化這個。 (至少可能有用,具體取決於周圍的代碼。更有可能這只是應用 AVX512 的一個有趣的練習,對大多數用例沒有用處)。
關鍵構建塊是AVX512F vpcompressd
:給定一個掩碼(例如來自 SIMD 比較),它會將選定的 dword 元素打亂到向量底部的連續元素。
一個 AVX512 ZMM / __m512i
向量保存 16x 32 位整數,因此我們只需要 2 個向量來保存每個可能的單位掩碼。 我們的輸入數字是一個掩碼,它選擇哪些元素應該是 output 的一部分。 (不需要將它廣播到向量和vptestmd
或類似的東西中;我們可以將其kmov
到掩碼寄存器中並直接使用它。)
另請參閱我在AVX2 上的 AVX512 答案什么是基於面罩左打包的最有效方法?
#include <stdint.h>
#include <immintrin.h>
// suggest 64-byte alignment for out_array
// returns count of set bits = length stored
unsigned bit_isolate_avx512(uint32_t out_array[32], uint32_t x)
{
const __m512i bitmasks_lo = _mm512_set_epi32(
1UL << 15, 1UL << 14, 1UL << 13, 1UL << 12,
1UL << 11, 1UL << 10, 1UL << 9, 1UL << 8,
1UL << 7, 1UL << 6, 1UL << 5, 1UL << 4,
1UL << 3, 1UL << 2, 1UL << 1, 1UL << 0
);
const __m512i bitmasks_hi = _mm512_slli_epi32(bitmasks_lo, 16); // compilers actually do constprop and load another 64-byte constant, but this is more readable in the source.
__mmask16 set_lo = x;
__mmask16 set_hi = x>>16;
int count_lo = _mm_popcnt_u32(set_lo); // doesn't actually cost a kmov, __mask16 is really just uint16_t
_mm512_mask_compressstoreu_epi32(out_array, set_lo, bitmasks_lo);
_mm512_mask_compressstoreu_epi32(out_array+count_lo, set_hi, bitmasks_hi);
return _mm_popcnt_u32(x);
}
與 Godbolt 上的 clang和 gcc 很好地編譯,除了 mov、movzx 和 popcnt 的幾個次優選擇,並且無緣無故地制作幀指針。 (它也可以用-march=knl
編譯;它不依賴於 AVX512BW 或 DQ。)
# clang9.0 -O3 -march=skylake-avx512
bit_isolate_avx512(unsigned int*, unsigned int):
movzx ecx, si
popcnt eax, esi
shr esi, 16
popcnt edx, ecx
kmovd k1, ecx
vmovdqa64 zmm0, zmmword ptr [rip + .LCPI0_0] # zmm0 = [1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768]
vpcompressd zmmword ptr [rdi] {k1}, zmm0
kmovd k1, esi
vmovdqa64 zmm0, zmmword ptr [rip + .LCPI0_1] # zmm0 = [65536,131072,262144,524288,1048576,2097152,4194304,8388608,16777216,33554432,67108864,134217728,268435456,536870912,1073741824,2147483648]
vpcompressd zmmword ptr [rdi + 4*rdx] {k1}, zmm0
vzeroupper
ret
在 Skylake-AVX512 上, vpcompressd zmm{k1}, zmm
為 2 微秒。輸入向量 -> output 的延遲為 3 個周期,但輸入掩碼 -> output 的延遲為 6 個周期。 ( https://www.uops.info/table.html / https://www.uops.info/html-instr/VPCOMPRESSD_ZMM_K_ZMM.html ). memory 目標版本是 4微指令:2p5 + 通常的存儲地址和存儲數據微指令,它們在較大指令的一部分時不能微熔斷。
壓縮成 ZMM reg 然后存儲(至少在第一次壓縮時)可能會更好,以節省總 uops。 第二個可能仍應利用vpcompressd [mem]{k1}
的屏蔽存儲功能,因此 output 陣列不需要填充即可踩到。 IDK 如果這有助於緩存行拆分,即掩碼是否可以避免在第二個緩存行中為具有全零掩碼的部分重放存儲 uop。
在 KNL 上, vpcompressd zmm{k1}
只是一個微指令。 Agner Fog 沒有使用 memory 目的地( https://agner.org/optimize/ )對其進行測試。
這是 Skylake-X 前端的 14 個融合域微指令,用於實際工作(例如,在內聯到多個x
值的循環后,因此我們可以將vmovdqa64
負載提升到循環之外。否則,這是另外 2 個微指令) . 所以前端瓶頸 = 14 / 4 = 3.5 個周期。
后端端口壓力:端口 5 為 6 uop(2x kmov(1) + 2x vpcompressd(2)):每 6 個周期 1 次迭代。 (即使在 IceLake (instlatx64 ) 上, vpcompressd
仍然是 2c 吞吐量,不幸的是,顯然 ICL 的額外 shuffle 端口不能處理這些 uops。並且kmovw k, r32
仍然是 1/clock,所以大概仍然是端口 5。 )
(其他端口很好:popcnt 在端口 1 上運行,當 512 位微指令在運行時,該端口的向量 ALU 被關閉。但不是它的標量 ALU,唯一一個處理 3 周期延遲 integer 指令的movzx dword, word
無法消除,只有 movzx dword, byte 可以做到,但它可以在任何端口上運行。)
延遲:integer 結果只是一個popcnt
(3 個周期)。 memory 結果的第一部分在掩碼准備好后存儲大約 7 個周期。 (kmov -> vpcompressd)。 vpcompressd 的向量源是一個常量,因此 OoO exec 可以盡早准備好它,除非它在緩存中丟失。
壓縮1<<0..15
常量是可能的,但可能不值得,通過移位構建它。 例如,使用vpmovzxbd
加載 16 字節_mm_setr_epi8(0..15)
,然后在 set1(1) 的向量上使用vpsllvd
(您可以從廣播中獲取或使用vpternlogd
+shift 即時生成)。 但這可能不值得,即使你用 asm 手工編寫(所以它是你的選擇,而不是編譯器),因為這已經使用了很多 shuffle,並且常量生成至少需要 3 或 4 條指令(每個至少有 6 個字節長;僅 EVEX 前綴每個就是 4 個字節)。
不過,我會從lo
轉換生成hi
部分,而不是單獨加載它。 除非周圍的代碼在端口 0 上遇到嚴重瓶頸,否則 ALU uop 並不比加載 uop 差。 一個 64 字節的常量填滿了整個高速緩存行。
您可以使用vpmovzxwd
加載壓縮 lo 常數:每個元素適合 16 位。 值得考慮是否可以將其提升到循環之外,這樣每次操作就不會花費額外的洗牌。
如果您想要 SIMD 向量中的結果而不是存儲到 memory,您可以將 2x vpcompressd
到寄存器中,並且可以使用count_lo
來查找vpermt2d
的隨機播放控制向量。 可能來自數組上的滑動窗口而不是 16x 64 字節向量? 但結果不能保證適合一個向量,除非您知道您的輸入設置了 16 位或更少的位。
64 位整數的情況更糟8x 64 位元素意味着我們需要 8 個向量。 因此,與標量相比,它可能不值得,除非您的輸入設置了很多位。
不過,您可以在循環中執行此操作,使用vpslld
by 8 來移動向量元素中的位。 你會認為kshiftrq
會很好,但是有 4 個周期的延遲,這是一個很長的循環攜帶的 dep 鏈。 無論如何,您都需要每個 8 位塊的標量 popcnt 來調整指針。 所以你的循環應該使用shr
/ kmov
和movzx
/ popcnt
。 (使用計數器 += 8 和bzhi
來喂 popcnt 會花費更多的 uops)。
循環攜帶的依賴項都很短(並且循環只運行 8 次迭代以覆蓋 64 位掩碼),因此亂序 exec 應該能夠很好地重疊工作以進行多次迭代。 尤其是如果我們展開 2,那么向量和掩碼依賴項可以在指針更新之前獲得。
vpslld
立即數,從向量常數開始shr r64, 8
以x
開頭。 (在移出所有位后變為 0 時可能會停止循環。這個 1 周期的 dep 鏈足夠短,可以讓 OoO exec 通過它到 zip 並隱藏大部分錯誤預測懲罰,當它發生時。)lea rdi, [rdi + rax*4]
其中 RAX 保存 popcnt 結果。 作品的rest跨迭代都是獨立的。 根據周圍的代碼,我們可能會在端口 5 上使用vpcompressd
和kmov
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.