在 32 位数字中查找第一个(最低)设置位的位置

Find position of first (lowest) set bit in 32-bit number

我需要得到一个32位数字中的1位数字,其中只有一个1位(总是)。 C++或asm中最快的方法。

例如

input:    0x00000001, 0x10000000
output:            0,         28

#ifdef __GNUC__,使用 __builtin_ctz(unsigned) 计算尾随零 (GCC manual)。 GCC、clang 和 ICC 在所有目标 ISA 上都支持它。 (在没有本机指令的 ISA 上,它将调用 GCC 辅助函数。)

Leading vs. Trailing 是按打印顺序书写时,MSB-first,如 8 位二进制 00000010 有 6 个前导零和一个尾随零。 (当转换为 32 位二进制时,将有 24+6 = 30 个前导零。)

对于 64 位整数,使用 __builtin_ctzll(unsigned long long)。不幸的是,GNU C bitscan 内置函数不采用 fixed-width 类型(尤其是 leading zeros 版本),但是 unsigned 在 GNU C 上始终是 32 位的x86(虽然不适用于 AVR 或 MSP430)。 unsigned long long 在我知道的所有 GNU C 目标上总是 uint64_t


在 x86 上,它将编译为 bsf or tzcnt depending on tuning + target options. tzcnt is a single uop with 3 cycle latency on modern Intel, and only 2 uops with 2 cycle latency on AMD (perhaps a bit-reverse to feed an lzcnt uop?) https://agner.org/optimize/ / https://uops.info/。无论哪种方式,它都得到快速硬件的直接支持,并且 比您在纯 C++ 中可以做的任何事情都快得多 。与 x * 1234567 的成本大致相同(在 Intel CPU 上,bsf/tzcntimul r, r, imm 的成本相同,在 front-end 微指令中,back-end端口和延迟。)

对于未设置位的输入,内置函数具有未定义的行为,允许它避免任何额外的检查,如果它可能 运行 为 bsf


在其他编译器(特别是 MSVC) 中,您可能需要 TZCNT 的内在函数,例如 _mm_tzcnt_32 来自 immintrin.h。 (Intel intrinsics guide)。或者您可能需要为 non-SIMD 内在函数包含 intrin.h (MSVC) 或 x86intrin.h

与 GCC/clang 不同,MSVC 不会阻止您将内部函数用于您尚未启用的 ISA 扩展,以便编译器自行使用。

MSVC 对于实际 BSF/BSR 也有 _BitScanForward / _BitScanReverse,但是 AMD 保证(英特尔也实现)的 leave-destination-unmodified 行为仍然没有被这些暴露内在函数,尽管它们 pointer-output API.


TZCNT 在没有 BMI1 的 CPU 上将 解码为 BSF,因为它的 machine-code 编码是 rep bsf。它们为 non-zero 输入给出相同的结果,因此编译器可以并且总是只使用 tzcnt 因为这在 AMD 上要快得多。 (它们在 Intel 上的速度相同,所以没有缺点。在 Skylake 和更高版本上,tzcnt 没有错误的输出依赖性。BSF 这样做是因为它在输入 = 0 时保持输出不变)。

(这种情况对于 bsrlzcnt 不太方便: bsr returns bit-index, lzcnt returns leading-zero计数。因此,为了在 AMD 上获得最佳性能,您需要知道您的代码只会 运行 在支持 BMI1 / TBM 的 CPU 上,以便编译器可以使用 lzcnt)

请注意,只要设置了 1 个位,从任一方向扫描都会找到相同的位。所以 31 - lzcnt = bsr 在这种情况下与 bsf = tzcnt 相同。如果移植到另一个只有 leading-zero 计数而没有 bit-reverse 指令的 ISA,可能会有用。


相关:

编译器确实可以识别 ffs() 并像内置函数一样内联它(就像它们对 memcpy 或 sqrt 所做的那样),但当您实际上想要一个基于 0 的索引。特别难告诉编译器只设置了 1 位。