簡體   English   中英

創建一個設置了 N 個最低有效位的掩碼

[英]Creating a mask with N least significant bits set

我想創建一個宏或函數1 mask(n) ,它給定一個數字n返回一個無符號整數,其中設置了n最低有效位。 盡管這看起來應該是一個基本原語,其中包含大量討論的可高效編譯的實現——但事實並非如此。

當然,對於像unsigned int這樣的原始整數類型,各種實現可能有不同的大小,所以為了具體起見,讓我們假設我們正在討論專門返回uint64_t雖然當然可接受的解決方案適用於任何無符號的(具有不同的定義)積分型。 特別是,當返回的類型等於或小於平台的本機寬度時,該解決方案應該是有效的。

至關重要的是,這必須適用於 [0, 64] 中的所有n 特別是mask(0) == 0mask(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 計數以及在許多情況下的吞吐量都是最佳的。)


在 C 中,除了 x86 Intel SnB 系列之外,這對於所有 64 位 ISA 都可以很好地編譯

不幸的是,即使在針對沒有 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 補碼表示)。

注意事項:

  • 1s 補充系統必須死——如果你有一個需要特殊的外殼
  • 一些編譯器可能不會優化 &63

當輸入 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 != 0sbb指令將進位標志設置為 0 或 -1。 移位長度64-N & 63被優化為-Nshr指令已經有一個隱式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.

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