将所有位从最低有效位翻转到最高有效最后 1 位值的最有效方法是什么?

0xd*_*eef 5 c x86 bit-manipulation bitmask bit-shift

举例来说,我有一个uint8_t可以是任何值的值,而我只想将所有位从最低有效位翻转到最高有效的最后 1 位值?我该如何以最有效的方式做到这一点?有没有一种解决方案可以避免使用循环?

以下是一些案例:

左边是原来的位,右边是翻转后的位。

  • 00011101->00000010
  • 00000000->00000000
  • 11111111->00000000
  • 11110111->00001000
  • 01000000->00111111

[编辑]

该类型也可以大于uint8_t,也可以是uint32_tuint64_t__uint128_t。我只是使用它uint8_t,因为它是示例案例中最容易显示的尺寸。

har*_*old 6

一般来说,我预计大多数解决方案大致具有以下形式:

  1. 计算需要翻转的位掩码
  2. 通过该掩码进行异或

正如评论中提到的,x64 是一个感兴趣的目标,在 x64 上你可以像这样执行步骤 1:

  • p通过前导零 ( _lzcnt_u64) 并从 64(或 32,以合适的为准)中减去该值,找到最高有效 1 的从 1 开始的位置。
  • 创建一个具有p从最低有效位开始的连续设置位的掩码,可能使用_bzhi_u64.

有一些变体,例如使用 BitScanReverse 查找最高有效的 1(但对于 0 的情况很丑陋),或者使用移位代替bzhi(但对于 64 的情况很丑陋)。lzcnt并且bzhi是一个很好的组合,没有丑陋的情况。bzhi需要 BMI2(Intel Haswell 或更高版本、AMD Zen 或更高版本)。

把它放在一起:

x ^ _bzhi_u64(~(uint64_t)0, 64 - _lzcnt_u64(x))
Run Code Online (Sandbox Code Playgroud)

这可以进一步简化为

_bzhi_u64(~x,  64 - _lzcnt_u64(x))
Run Code Online (Sandbox Code Playgroud)

正如彼得所示。这并不遵循最初的两步计划,而是翻转所有位,然后重置最初为前导零的位。

由于那些原始的前导零形成了 中前导 1 的连续序列~x,替代方案bzhi可以是将 2 的适当幂添加到~x(尽管有时是零,这可能被认为是 2 64,将设置位刚好超出 的顶部)号码)。不幸的是,我们需要的 2 的幂计算起来有点烦人,至少我无法想出一个好的方法来做到这一点,这对我来说似乎是一个死胡同。

步骤 1 也可以使用一些移位和按位 OR 以通用方式(无特殊操作)实现,如下所示:

// Get all-ones below the leading 1
// On x86-64, this is probably slower than Paul R's method using BSR and shift
//   even though you have to special case x==0
m = x | (x >> 1);
m |= m >> 2;
m |= m >> 4;
m |= m >> 8;
m |= m >> 16;
m |= m >> 32;  // last step should be removed if x is 32-bit
Run Code Online (Sandbox Code Playgroud)

AMD CPU 的 BSR 较慢(但 LZCNT 较快;https://uops.info/),因此您可能需要此转换/或版本的uint8_tor uint16_t(需要最少的步骤),特别是如果您需要与所有 CPU 兼容提高速度AMD 比 Intel 更重要。

这个通用版本在 SIMD 元素中也很有用,尤其是窄元素,在 AVX-512 之前我们没有前导零计数。

  • @kabibesadagat:你永远不需要`m = x | 的“通用”版本 (x>>1)` x86 CPU 上的东西。您始终至少有 __builtin_clzll` 或等效项,它们在最坏的情况下可以编译为 BSR 指令,因此您需要特殊情况为零。或者在32位模式下,两条BSR指令对半,以及其他检查。但无论如何,如果您没有,则需要替换“bzhi”部分,而不是“lzcnt”部分。 (2认同)