#ifdef __GNUC__,使用__builtin_ctz(unsigned) 计算尾随零 (GCC manual)。 GCC、clang 和 ICC 在所有目标 ISA 上都支持它。 (在没有本地指令的 ISA 上,它将调用 GCC 辅助函数。)
Leading vs. Trailing 是以打印顺序编写时,MSB 优先,如 8 位二进制 00000010 有 6 个前导零和一个尾随零。 (当转换为 32 位二进制时,将有 24+6 = 30 个前导零。)
对于 64 位整数,请使用 __builtin_ctzll(unsigned long long)。不幸的是,GNU C bitscan 内置函数不采用固定宽度类型(尤其是 leading zeros 版本),但unsigned 在 GNU C for x86 上始终是 32 位的(尽管不适用于 AVR 或MSP430)。在我知道的所有 GNU C 目标上,unsigned long long 始终是 uint64_t。
在 x86 上,它将编译为 bsf 或 tzcnt,具体取决于调整 + 目标选项。 tzcnt 是在现代 Intel 上具有 3 个周期延迟的单 uop,而只有 2 uop在 AMD 上具有 2 个周期延迟(可能是位反转来馈送 lzcnt uop?)https://agner.org/optimize/ / https://uops.info/。无论哪种方式,它都受到快速硬件的直接支持,并且比您在纯 C++ 中所做的任何事情都快得多。与x * 1234567 的成本大致相同(在 Intel CPU 上,bsf/tzcnt 的成本与imul r, r, imm 相同,在前端微指令、后端端口和延迟方面。)
对于没有设置位的输入,内置函数具有未定义的行为,因此可以避免任何额外的检查,如果它可能以 bsf 运行。
在其他编译器(特别是 MSVC) 中,您可能需要 TZCNT 的内在函数,例如来自 immintrin.h 的 _mm_tzcnt_32。 (Intel intrinsics guide)。或者,对于非 SIMD 内部函数,您可能需要包含 intrin.h (MSVC) 或 x86intrin.h。
与 GCC/clang 不同,MSVC 不会阻止您将内在函数用于您尚未启用编译器自行使用的 ISA 扩展。
MSVC 也有 _BitScanForward / _BitScanReverse 用于实际的 BSF/BSR,但是 AMD 保证(以及 Intel 也实现)的未修改离开目的地的行为仍然没有被这些内在函数公开,尽管它们的指针输出 API .
TZCNT 在没有 BMI1 的 CPU 上解码 as BSF,因为它的机器代码编码是 rep bsf。它们为非零输入提供相同的结果,因此编译器可以并且总是只使用tzcnt,因为这在 AMD 上要快得多。 (它们在 Intel 上的速度相同,因此没有缺点。在 Skylake 及更高版本上,tzcnt 没有错误的输出依赖性。BSF 这样做是因为它在 input=0 时保持其输出不变)。
(bsr 与 lzcnt 相比,这种情况不太方便:bsr 返回位索引,lzcnt 返回前导零计数。因此,为了在 AMD 上获得最佳性能,您需要知道您的代码只会在支持 BMI1 / TBM 的 CPU 上运行,因此编译器可以使用 lzcnt)
请注意,只要设置了 1 个位,从任一方向扫描都会找到相同的位。所以31 - lzcnt = bsr 在这种情况下与bsf = tzcnt 相同。如果移植到另一个只有前导零计数且没有位反转指令的 ISA,可能很有用。
相关:
编译器确实可以识别ffs() 并像内置函数一样内联它(就像他们为 memcpy 或 sqrt 所做的那样),但当你真正想要一个 0 时,并不总是设法优化他们的固定序列为实现它所做的所有工作- 基于索引。告诉编译器只有 1 位设置尤其困难。