【问题标题】:Find position of first (lowest) set bit in 32-bit number查找 32 位数中第一个(最低)设置位的位置
【发布时间】:2020-02-14 18:18:38
【问题描述】:

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

例如

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

【问题讨论】:

  • 你关心什么汇编语言?每个处理器架构都有自己的汇编语言。对于 x86,请考虑使用 bsf,它完全符合您的要求。
  • 有很多方法可以解决这个问题。您已经尝试过什么,您在哪里遇到问题?
  • 数字右侧第 28 位。 x86
  • 所以你或多或少想要这个:en.wikipedia.org/wiki/Find_first_set ?
  • @Someprogrammerdude:这不是调试问题。不需要 MCVE,只需要所需的输出就可以了。 IMO 它足够晦涩,问题也足够简单,只要求适当的内在本质就可以了。不像我们期望尝试的“反转数组”作业问题。

标签: c++ assembly x86 bit-manipulation intrinsics


【解决方案1】:

#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 上,它将编译为 bsftzcnt,具体取决于调整 + 目标选项。 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 时保持其输出不变)。

(bsrlzcnt 相比,这种情况不太方便:bsr 返回位索引,lzcnt 返回前导零计数。因此,为了在 AMD 上获得最佳性能,您需要知道您的代码只会在支持 BMI1 / TBM 的 CPU 上运行,因此编译器可以使用 lzcnt)

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


相关:

编译器确实可以识别ffs() 并像内置函数一样内联它(就像他们为 memcpy 或 sqrt 所做的那样),但当你真正想要一个 0 时,并不总是设法优化他们的固定序列为实现它所做的所有工作- 基于索引。告诉编译器只有 1 位设置尤其困难。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-10-19
    • 1970-01-01
    • 2011-03-09
    • 1970-01-01
    • 1970-01-01
    • 2019-09-28
    • 2021-07-06
    • 1970-01-01
    相关资源
    最近更新 更多