![](/img/trans.png)
[英]Check that all bits are set except the Least Significant Bit by using masks
[英]Creating a mask with N least significant bits set
我想創建一個宏或函數1 mask(n)
,它給定一個數字n
返回一個無符號整數,其中設置了n
最低有效位。 盡管這看起來應該是一個基本原語,其中包含大量討論的可高效編譯的實現——但事實並非如此。
當然,對於像unsigned int
這樣的原始整數類型,各種實現可能有不同的大小,所以為了具體起見,讓我們假設我們正在討論專門返回uint64_t
雖然當然可接受的解決方案適用於任何無符號的(具有不同的定義)積分型。 特別是,當返回的類型等於或小於平台的本機寬度時,該解決方案應該是有效的。
至關重要的是,這必須適用於 [0, 64] 中的所有n
。 特別是mask(0) == 0
和mask(64) == (uint64_t)-1
。 許多“明顯”的解決方案不適用於這兩種情況之一。
最重要的標准是正確性:只有不依賴於未定義行為的正確解決方案才是有趣的。
第二個最重要的標准是性能:理想情況下,習語應該編譯為近似最有效的特定於平台的方式,以便在通用平台上執行此操作。
以性能的名義犧牲簡單性的解決方案,例如,在不同平台上使用不同的實現,是好的。
1最一般的情況是函數,但理想情況下,它也可以用作宏,而無需多次重新評估其任何參數。
沒有分支的另一種解決方案
unsigned long long mask(unsigned n)
{
return ((1ULL << (n & 0x3F)) & -(n != 64)) - 1;
}
n & 0x3F
將移位量保持為最大 63,以避免 UB。 事實上,大多數現代建築只會搶偏移量的低位,所以沒有and
需要為這個指令。
可以將 64 的檢查條件更改為-(n < 64)
以使其返回 n ⩾ 64 的所有_bzhi_u64(-1ULL, (uint8_t)n)
如果您的 CPU 支持BMI2 ,則相當於_bzhi_u64(-1ULL, (uint8_t)n)
。
Clang 的輸出看起來比 gcc 好。 碰巧 gcc 為 MIPS64 和 ARM64 發出條件指令,但不為 x86-64 發出條件指令,從而導致更長的輸出
條件也可以簡化為n >> 6
,利用如果 n = 64 它將是 1 的事實。我們可以從結果中減去它,而不是像上面那樣創建掩碼
return (1ULL << (n & 0x3F)) - (n == 64) - 1; // or n >= 64
return (1ULL << (n & 0x3F)) - (n >> 6) - 1;
gcc 將后者編譯為
mov eax, 1
shlx rax, rax, rdi
shr edi, 6
dec rax
sub rax, rdi
ret
更多的選擇
return ~((~0ULL << (n & 0x3F)) << (n == 64));
return ((1ULL << (n & 0x3F)) - 1) | (((uint64_t)n >> 6) << 63);
return (uint64_t)(((__uint128_t)1 << n) - 1); // if a 128-bit type is available
32 位的類似問題: Set last `n` bits in unsigned int
嘗試
unsigned long long mask(const unsigned n)
{
assert(n <= 64);
return (n == 64) ? 0xFFFFFFFFFFFFFFFFULL :
(1ULL << n) - 1ULL;
}
有幾個很好的、聰明的答案可以避免條件,但是現代編譯器可以為此生成不分支的代碼。
您的編譯器可能會想辦法將其內聯,但您可以使用inline
或在 C++ 中的constexpr
給它一個提示。
unsigned long long int
類型保證至少為 64 位寬,並且出現在每個實現中,而uint64_t
不是。
如果你需要一個宏(因為你需要一些可以作為編譯時常量的東西),那可能是:
#define mask(n) ((64U == (n)) ? 0xFFFFFFFFFFFFFFFFULL : (1ULL << (unsigned)(n)) - 1ULL)
正如幾個人在評論中正確提醒我的那樣, 1ULL << 64U
是潛在的未定義行為! 因此,請為該特殊情況插入檢查。
如果在超過 64 位的實現上支持該類型的全部范圍對您很重要,您可以用CHAR_BITS*sizeof(unsigned long long)
替換64U
。
您可以類似地從無符號右移生成它,但您仍然需要檢查n == 64
作為特殊情況,因為按類型的寬度右移是未定義的行為。
(N1570 草案)標准的相關部分說,左右位移位:
如果右操作數的值為負或大於或等於提升的左操作數的寬度,則行為未定義。
這讓我絆倒了。 再次感謝評論中的每個人,他們審查了我的代碼並向我指出了錯誤。
這是一個可移植且無條件的:
unsigned long long mask(unsigned n)
{
assert (n <= sizeof(unsigned long long) * CHAR_BIT);
return (1ULL << (n/2) << (n-(n/2))) - 1;
}
0
不是必需的輸出時才有效,但效率更高。 2 n+1 - 1 計算無溢出。 即設置了低n
位的整數,對於 n = 0 .. all_bits
可能在cmov
的三元組中使用cmov
可能是解決問題中完整問題的更有效的解決方案。 也許基於設置 MSB 的數字向左旋轉,而不是向左移動1
,以解決計數與pow2
計算問題之間的pow2
。
// defined for n=0 .. sizeof(unsigned long long)*CHAR_BIT
unsigned long long setbits_upto(unsigned n) {
unsigned long long pow2 = 1ULL << n;
return pow2*2 - 1; // one more shift, and subtract 1.
}
編譯器輸出建議一個替代版本,如果你不使用 gcc/clang(已經這樣做了),那么在某些 ISA 上很好:烘烤額外的移位計數,以便初始移位可以移出所有位,留下0 - 1 =
所有位設置。
unsigned long long setbits_upto2(unsigned n) {
unsigned long long pow2 = 2ULL << n; // bake in the extra shift count
return pow2 - 1;
}
此函數的 32 位版本的輸入/輸出表是:
n -> 1<<n -> *2 - 1
0 -> 1 -> 1 = 2 - 1
1 -> 2 -> 3 = 4 - 1
2 -> 4 -> 7 = 8 - 1
3 -> 8 -> 15 = 16 - 1
...
30 -> 0x40000000 -> 0x7FFFFFFF = 0x80000000 - 1
31 -> 0x80000000 -> 0xFFFFFFFF = 0 - 1
您可以在它之后打一個cmov
,或者其他處理必須產生零的輸入的方式。
在 x86 上,我們可以使用 3 條單 uop 指令有效地計算它:(或者 Ryzen 上的 BTS 為 2 uop)。
xor eax, eax
bts rax, rdi ; rax = 1<<(n&63)
lea rax, [rax + rax - 1] ; one more left shift, and subtract
(3 組件 LEA 在 Intel 上有 3 個周期延遲,但我相信這對於 uop 計數以及在許多情況下的吞吐量都是最佳的。)
不幸的是,即使在針對沒有 BMI2 的 Intel CPU(其中shl reg,cl
為 3 uops)進行調優時,C 編譯器也很笨拙並且會錯過使用bts
。
例如,gcc 和 clang 都在 Godbolt 上執行此操作(使用 dec 或添加 -1)
# gcc9.1 -O3 -mtune=haswell
setbits_upto(unsigned int):
mov ecx, edi
mov eax, 2 ; bake in the extra shift by 1.
sal rax, cl
dec rax
ret
由於 Windows x64 調用約定,MSVC 在 ECX 中以n
開頭,但對其取模,它和 ICC 做同樣的事情:
# ICC19
setbits_upto(unsigned int):
mov eax, 1 #3.21
mov ecx, edi #2.39
shl rax, cl #2.39
lea rax, QWORD PTR [-1+rax+rax] #3.21
ret #3.21
使用 BMI2 ( -march=haswell
),我們可以使用-march=haswell
從 gcc/clang 中獲得最適合 AMD 的代碼
mov eax, 2
shlx rax, rax, rdi
add rax, -1
ICC 仍然使用 3 分量 LEA,因此如果您的目標是 MSVC 或 ICC,無論您是否啟用 BMI2,都在源代碼中使用2ULL << n
版本,因為無論哪種方式您都無法獲得 BTS。 這避免了兩全其美的情況; 慢 LEA 和可變計數移位而不是 BTS。
在非 x86 ISA 上(大概可變計數移位是有效的,因為如果計數恰好為零,它們沒有 x86 不修改標志的稅,並且可以使用任何寄存器作為計數),這編譯得很好。
例如 AArch64。 當然,這可以提升常數2
以便使用不同的n
重用,就像 x86 可以使用 BMI2 shlx
。
setbits_upto(unsigned int):
mov x1, 2
lsl x0, x1, x0
sub x0, x0, #1
ret
在 PowerPC、RISC-V 等上基本相同。
#include <stdint.h>
uint64_t mask_n_bits(const unsigned n){
uint64_t ret = n < 64;
ret <<= n&63; //the &63 is typically optimized away
ret -= 1;
return ret;
}
結果:
mask_n_bits:
xor eax, eax
cmp edi, 63
setbe al
shlx rax, rax, rdi
dec rax
ret
返回預期結果,如果傳遞一個常量值,它將被優化為 clang 和 gcc 中的常量掩碼以及 -O2(但不是 -Os)的 icc。
解釋:
&63 得到優化,但確保偏移 <=64。
對於小於 64 的值,它只使用(1<<n)-1
設置前 n 位。 1<<n
設置第 n 位(等效 pow(2,n)),並從 2 的冪中減去 1 設置小於該值的所有位。
通過使用條件來設置要移位的初始 1,不會創建分支,但它為所有 >=64 的值提供 0,因為左移 0 將始終產生 0。因此,當我們減去 1 時,我們得到所有位設置為 64 和更大的值(因為 -1 的 2s 補碼表示)。
注意事項:
當輸入 N 在 1 到 64 之間時,我們可以使用-uint64_t(1) >> (64-N & 63)
。
常量 -1 有 64 個設置位,我們將其中的 64-N 個移開,所以我們剩下 N 個設置位。
當 N=0 時,我們可以在移位前使常數為零:
uint64_t mask(unsigned N)
{
return -uint64_t(N != 0) >> (64-N & 63);
}
這在 x64 clang 中編譯為 5 條指令。 neg指令將進位標志設置為N != 0
, sbb指令將進位標志設置為 0 或 -1。 移位長度64-N & 63
被優化為-N
: shr指令已經有一個隱式shift_length & 63
。
mov rcx,rdi
neg rcx
sbb rax,rax
shr rax,cl
ret
使用 BMI2 擴展,它只有四個指令(移位長度可以保持在rdi 中):
neg edi
sbb rax,rax
shrx rax,rax,rdi
ret
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.