簡體   English   中英

設置任意大小的前導零位 integer C++

[英]Set the leading zero bits in any size integer C++

我想在標准 C++ 中將任何大小 integer 的前導零位設置為 1。

例如。

0001 0011 0101 1111 -> 1111 0011 0101 1111

我發現的所有算法都需要相當昂貴的前導零計數。 然而,這很奇怪。 有非常快速和簡單的方法來進行其他類型的位操作,例如:

 int y = -x & x; //Extracts lowest set bit, 1110 0101 -> 0000 0001

 int y = (x + 1) & x; //Will clear the trailing ones, 1110 0101 - > 1110 0100

 int y = (x - 1) | x; //Will set the trailing zeros, 0110 0100 - > 0110 0111

所以這讓我認為必須有一種方法可以在一個由基本位操作符組成的簡單代碼行中設置 integer 的前導零。 請告訴我有希望,因為現在我正在解決我的 integer 中位的順序,然后使用設置尾隨零的快速方法,然后再次反轉 integer 以將我的前導零設置為 1。 這實際上比使用前導零計數要快得多,但與上述其他算法相比仍然相當慢。

 template<typename T>
 inline constexpr void reverse(T& x)
 {
    T rev = 0;
    size_t s = sizeof(T) * CHAR_BIT;

    while(s > 0)
    {
        rev = (rev << 1) | (x & 0x01);
        x >>= 1;
        s -= 1uz;
    }//End while

    x = rev;
 }

 
 template<typename T>
 inline constexpr void set_leading_zeros(T& x)
 {

     reverse(x);

     x = (x - 1) | x;//Set trailing 0s to 1s
     
     reverse(x);
 }

編輯

因為有人問:我正在使用運行在 CPU 上的 MS-DOS,從早期的 X86 到安裝在舊 CNC 機器上的 486DX。 娛樂時間。 :D

可以設置前導零而不計算它們,同時還可以避免反轉 integer。 為方便起見,我不會為通用的 integer 類型 T 執行此操作,但可能可以對其進行調整,或者您可以使用模板專業化。

首先通過向下“擴展”位來計算所有不是前導零的位的掩碼:

uint64_t m = x | (x >> 1);
m |= m >> 2;
m |= m >> 4;
m |= m >> 8;
m |= m >> 16;
m |= m >> 32;

然后設置該掩碼未覆蓋的所有位:

return x | ~m;

獎勵:即使在x = 0並且x已設置所有位時,這也會自動起作用,其中一個在計數前導零方法中可能導致過大的移位量(取決於細節,但幾乎總是它們很麻煩,因為有 65 個不同的情況,但只有 64 個有效的班次量,如果我們談論的是uint64_t )。

您可以使用std::countl_zero計算前導零並創建一個位掩碼,您的位掩碼與原始值按位或:

#include <bit>
#include <climits>
#include <type_traits>

template<class T>
requires std::is_unsigned_v<T>
T leading_ones(T v) {
    auto lz = std::countl_zero(v);
    return lz ? v | ~T{} << (CHAR_BIT * sizeof v - lz) : v;
}

如果您有std::uint16_t ,例如

0b0001001101011111

那么~T{}0b1111111111111111CHAR_BIT * sizeof v是 16 並且countl_zero(v)3 左移0b1111111111111111 16-3 步:

0b1110000000000000

與原件按位或:

  0b0001001101011111
| 0b1110000000000000
--------------------
= 0b1111001101011111

你的reverse速度非常慢! 使用N位 int 您需要N次迭代來反轉,每個至少 6 條指令,然后至少 2 條指令來設置尾隨位,最后N次迭代以再次反轉值。 OTOH即使是最簡單的前導零計數也只需要N次迭代,然后直接設置前導位:

template<typename T>
inline constexpr T trivial_ilog2(T x) // Slow, don't use this
{
    if (x == 0) return 0;

    size_t c{};
    while(x)
    {
        x >>= 1;
        c += 1u;
    }

    return c;
}

template<typename T>
inline constexpr T set_leading_zeros(T x)
{
    if (std::make_unsigned_t(x) >> (sizeof(T) * CHAR_BIT - 1)) // top bit is set
        return x;
    return x | (-T(1) << trivial_ilog2(x));
}

x = set_leading_zeros(x);

還有許多其他方法可以更快地計算前導零/獲取 integer 對數。 其中一種方法涉及以 2 的冪為步驟進行操作,例如如何在 harold 的回答中創建掩碼:

但是,由於您的目標是特定目標,而不是做跨平台的事情,並且想要壓縮每一點性能,因此幾乎沒有理由使用純標准功能,因為這些用例通常需要特定於平台的代碼。 如果內在函數可用,您應該使用它,例如在現代 C++ 中有std::countl_zero但每個編譯器已經有內在函數可以做到這一點,這將 map 到該平台的最佳指令序列,例如_BitScanReverse__builtin_clz

如果內在函數不可用或性能仍然不夠,請嘗試查找表 例如,這是一個包含 256 個元素的日志表的解決方案

static const char LogTable256[256] = 
{
#define LT(n) n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n
    -1, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
    LT(4), LT(5), LT(5), LT(6), LT(6), LT(6), LT(6),
    LT(7), LT(7), LT(7), LT(7), LT(7), LT(7), LT(7), LT(7)
};

uint16_t lut_ilog2_16(uint16_t x)
{
    uint8_t h = x >> 8;
    if (h) return LogTable256[h] + 8;
    else return LogTable256[x & 0xFF];
}

set_leading_zeros ,只需像上面一樣調用lut_ilog2_16

日志表更好的解決方案是掩碼表,這樣您就可以直接獲取掩碼而不是計算1 << LogTable256[x]

static const char MaskTable256[256] =
{
    0xFF, 0xFE, 0xFC...
}

其他一些注意事項:

  • 1uz不是 C++ 中的有效后綴
  • 不要將引用用於適合單個 integer 的小型類型。 這不是必需的,並且在不內聯時通常會更慢。 只需將結果從 function 分配回來

(工作正在進行中,這里剛剛斷電;現在發布以保存我的工作。)

Crusty old x86 CPUs have very slow C++20 std::countl_zero / GNU C __builtin_clz ( 386 bsr = Bit Scan Reverse actually finds the position of the highest set bit, like 31-clz, and is weird for an input of 0 so您需要對此進行分支。)對於 Pentium Pro / Pentium II 之前的 CPU,Harold 的答案是您最好的選擇,直接生成掩碼而不是計數。

(在 386 之前,使用mov al, ah / mov ah, 0而不是shr ax, 8之類的部分寄存器惡作劇可能會更好地進行大量移位,因為 286 和更早版本沒有用於恆定時間移位的桶形移位器。但是在 C++ 中,編譯器需要弄清楚這一點。移位 16 位是免費的,因為 32 位 integer 只能保存在 286 或更早版本的一對 16 位寄存器中。)

  • 8086 到 286 - 沒有可用的指令。

  • 386: bsf / bsr : 10+3n 個周期。 最壞情況: 10+3*31 = 103c

  • 486bsf (16或32位寄存器):6-42個周期; bsr 7-104 個周期(16 位寄存器少 1 個周期)。

  • P5 Pentium: bsf :6-42 個周期(16 位為 6-34 個); bsr 7-71 周期。 (或 7-39 為 16 位)。 不可配對。

  • Intel P6 及更高版本: bsr / bsr :1 uop,1 周期吞吐量,3 周期延遲 (PPro / PII 及更高版本)。

  • AMD K7/K8/K10/Bulldozer/Zen: bsf / bsr對於現代 CPU 來說速度較慢。 例如 K10 3 周期吞吐量,4 周期延遲,6 / 7 m-ops。

  • Intel Haswell / AMD K10:引入了lzcnt (作為 Intel BMI1 的一部分,或者在tzcnt和 BMI1 的 rest 之前為 AMD 提供了自己的功能位)。
    對於 0 的輸入,它們返回操作數大小,因此它們分別完全實現 C++20 std::countl_zero / countr_zero ,這與bsr / bsf不同。 (在輸入 = 0 時未修改目標。AMD 記錄了這一點,英特爾至少在當前 CPU 上實際實現了它,但將目標寄存器記錄為“未定義”內容。也許一些較舊的英特爾 CPU 有所不同,否則只是煩人他們沒有記錄行為,因此軟件可以利用。)

    在 AMD 上,它們是 lzcnt 的快速、單一的微指令,而lzcnt tzcnt需要一個(可能是位反轉來饋送lzcnt執行單元),因此與bsf / bsr相比,這是一場不錯的勝利。 這就是為什么編譯器在countr_zero / __builtin_ctz時通常使用rep bsf的原因,因此它將在支持它的 CPU 上以tzcnt運行,但在較舊的 CPU 上以bsf運行。 bsr / lzcnt不同,它們對非零輸入產生相同的結果。

    在 Intel 上,與bsf / bsr相同的快速性能,甚至包括output 依賴項,直到 Skylake 修復該問題; 它是bsf / bsr的真正依賴項,但tzcnt / lzcntpopcnt的錯誤依賴項。


具有位掃描構建塊的快速算法

但是在 P6 (Pentium Pro) 及更高版本上,對最高設置位的位掃描可能是一個有用的構建塊,用於比 log2(width) 移位/或操作更快的策略,特別是對於 64 位上的uint64_t機器。 (或者對於 32 位機器上的uint64_t可能更是如此,其中每次移位都需要在間隙中移位位。)

來自https://www2.math.uni-wuppertal.de/~fpf/Uebungen/GdR-SS02/opcode_i.html的周期計數,其中包含 8088 到 Pentium 的指令時序。 (但不包括通常支配 8086 尤其是 8088 性能的取指令瓶頸。)

bsr (最高設置位的索引)在現代 x86很快:P6 及更高版本的 1 個周期吞吐量,在 AMD 上還不錯。 在最近的 x86 上,BMI1 lzcnt在 AMD 上也是 1 個周期,並且避免了 output 依賴(在 Skylake 和更新版本上)。 它也適用於0的輸入(產生類型寬度,也就是操作數大小),與bsr不同,它使目標寄存器保持不變。

我認為最好的版本(如果 BMI2 可用)是受 Ted Lyngmo 回答的啟發,但改為向左/向右移動而不是生成蒙版。 ISO C++ 不保證>>是有符號 integer 類型的算術右移,但所有理智的編譯器都選擇它作為其實現定義的行為。 (例如,GNU C 記錄了它。)

https://godbolt.org/z/hKohn8W8a有這個想法,如果我們不需要處理 x==0,這確實很棒。

如果我們正在考慮可用的 BMI2 有什么效率的話,這也是 BMI2 bzhi 的一個想法。 喜歡x | ~ _bzhi_u32(-1, 32-lz); x | ~ _bzhi_u32(-1, 32-lz); 不幸的是需要兩個反轉, 32-lzcnt~ 我們有 BMI1 andn ,但沒有等價的orn 而且我們不能只使用neg ,因為bzhi不會掩蓋計數; 這就是重點,它對 33 種不同的輸入具有獨特的行為。 明天可能會發布這些作為答案。


int set_leading_zeros(int x){
    int lz = __builtin_clz(x|1);                // clamp the lzcount to 31 at most
    int tmp = (x<<lz);                          // shift out leading zeros, leaving a 1 (or 0 if x==0)
    tmp |= 1ULL<<(CHAR_BIT * sizeof(tmp) - 1);  // set the MSB in case x==0
    return tmp>>lz;                             // sign-extend with an arithmetic right shift.
}
#include <immintrin.h>
uint32_t set_leading_zeros_bmi2(uint32_t x){
    int32_t lz = _lzcnt_u32(x);            // returns 0 to 32 
    uint32_t mask = _bzhi_u32(-1, lz);     // handles all 33 possible values, producing 0 for lz=32
    return x | ~mask;
}

在 x86-64 上,您可以

與 BMI2 shlx / sarx結合使用,即使在 Intel CPU 上也可實現單微指令可變計數移位。

通過有效的轉換(BMI2,或非英特爾,如 AMD),最好使用(x << lz) >> lz進行符號擴展。 除非lz是類型寬度; 如果您需要處理這個問題,生成掩碼可能更有效。

不幸的是shl/sar reg, cl在 Sandybridge 系列上花費 3 uops(因為 x86 遺留行李,如果計數恰好為零,則班次不會設置 FLAGS),因此您需要 BMI2 shlx / sarx才能使其優於bsr ecx, dsr / mov tmp, -1 / not ecx / shl tmp, cl / or dst,reg

暫無
暫無

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

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