【问题标题】:Counting the number of leading zeros in a 128-bit integer计算 128 位整数中前导零的数量
【发布时间】:2015-02-10 03:09:30
【问题描述】:

如何有效地计算 128 位整数 (uint128_t) 中前导零的数量?

我知道 GCC 的内置函数:

  • __builtin_clz, __builtin_clzl, __builtin_clzll
  • __builtin_ffs, __builtin_ffsl, __builtin_ffsll

但是,这些函数仅适用于 32 位和 64 位整数。

我还找到了一些 SSE 说明:

  • __lzcnt16, __lzcnt, __lzcnt64

您可能猜到了,这些仅适用于 16 位、32 位和 64 位整数。

对于 128 位整数,是否有任何类似的高效内置功能?

【问题讨论】:

  • 我假设解决两个 64 位整数,然后组合,对你来说太贵了?
  • 好吧,我必须这样做,前提是没有人知道更好的解决方案。但是,一条指令可能比整个移位、转换、分支等内容更有效,也更美观。
  • 你可以把丑陋的东西包裹在一个函数里。
  • 是什么让您认为您的 uint128_t 正在使用 SSE 寄存器?无论如何,它很可能使用两个 64 位寄存器。 SSE registers don't have a 128-bit FLAGS register so they are less useful for big integer arithmeticbsrlzcnt 指令设置零和进位标志,因此您应该能够利用它来发挥自己的优势。

标签: c++ gcc bit-manipulation sse


【解决方案1】:
inline int clz_u128 (uint128_t u) {
  uint64_t hi = u>>64;
  uint64_t lo = u;
  int retval[3]={
    __builtin_clzll(hi),
    __builtin_clzll(lo)+64,
    128
  };
  int idx = !hi + ((!lo)&(!hi));
  return retval[idx];
}

这是一个无分支的变体。请注意,与分支解决方案相比,完成的工作更多,并且在实践中分支可能是可预测的。

它还依赖于__builtin_clzll 在输入 0 时不会崩溃:文档说结果未定义,但它只是未指定还是未定义?

【讨论】:

  • 必须将+ 64 附加到__builtin_clzll(lo),对吗? __builtin_clzll() 的等效替代方案适用于所有输入是64 - __builtin_ffsll()
  • @user1494080 是的,哎呀。如果__builtin_clzll undefinedness 只是“我输出垃圾”,我们没问题。如果是“我崩溃”,那就不行了。我不知道“未定义”是什么意思;也许他们在某个地方定义了他们的意思。
  • 我接受了这种方法,因为它在我的应用程序中比其他方法快一点。
  • 我无法想象它会“崩溃”的场景 - 我认为这就像尝试将 (shlq/shrq) 移位超过 63 位位置。从技术上讲,英特尔可能已经为自己留出了在未来犯错的空间,但是......它永远不会发生 - 它会破坏同样忽略零值结果的旧代码。知道-mlzcnt 标志生成了什么代码会很有趣。
  • 顺便说一句,在英特尔的实现中,实际行为是目标寄存器未修改,因为他们选择超越 ISA 规范(不破坏某些特定的遗留代码是通常的原因)。另请参阅我在my Collatz-conjecture asm answer 中对 TZCNT 的类似行为的讨论,因为情况相同,只是当您不确定目标 CPU 是否会将其解码为 BSR 或LZCNT:它们返回相反的结果。 (与 TZCNT / BSF 不同)。
【解决方案2】:

假设一个“随机”分布,第一个非零位将在高 64 位中,具有压倒性的概率,因此首先测试那一半是有意义的。

查看生成的代码:

/* inline */ int clz_u128 (uint128_t u)
{
    unsigned long long hi, lo; /* (or uint64_t) */
    int b = 128;

    if ((hi = u >> 64) != 0) {
        b = __builtin_clzll(hi);
    }
    else if ((lo = u & ~0ULL) != 0) {
        b = __builtin_clzll(lo) + 64;
    }

    return b;
}

我希望 gcc 使用 bsrq 指令实现每个 __builtin_clzll - 位扫描反向,即最高有效位位置 - 结合 xor(msb ^ 63)sub、@ 987654327@,将其转换为前导零计数。 gcc 可能会使用正确的 -march=(架构)选项生成 lzcnt 指令。


编辑:其他人指出在这种情况下“分布”不相关,因为无论如何都需要测试 HI uint64_t。

【讨论】:

  • 取决于什么随机分布。对于整个空间的均匀分布,绝对是。对于其他人,也许不是。
  • 多么多枝!我会被 3 大小的数组查找解决方案所吸引。但是分支预测可能会使上述速度更快。是的,除非 128 but 值是随机非理性的小数部分,否则均匀随机分布的假设是有问题的。但是,即使没有这种假设,您也需要测试非零高 dqword:试试吧。这个假设是红鲱鱼。
  • @BrettHale 分布看起来如何并不明显,但它不是均匀分布。但是,我总是必须先测试高位,不是吗?另一个问题:& ~0ULL 部分是否必要?缩小转换不是总是自动截断高位吗?
  • @Yakk 你能多写几句关于你的查找解决方案吗?我还没想好。
  • 只是想分享一些研究:根据 C++ 标准的 § 4.7 [conv.integral],& ~0ULL 部分不是必需的。
【解决方案3】:

只要 gcc 支持,Yakk 的答案适用于各种目标 目标的 128 位整数。但是,请注意,在 x86-64 平台上, 使用英特尔 Haswell 处理器或更新的处理器,有一个更有效的解决方案:

#include <immintrin.h>
#include <stdint.h>
// tested with compiler options: gcc -O3 -Wall -m64  -mlzcnt

inline int lzcnt_u128 (unsigned __int128 u) {
  uint64_t hi = u>>64;
  uint64_t lo = u;
  lo = (hi == 0) ? lo : -1ULL;
  return _lzcnt_u64(hi) + _lzcnt_u64(lo);
}

_lzcnt_u64 内部函数编译 (gcc 5.4) 为 lzcnt 指令,这很好 为零输入定义(它返回 64),与 gcc 的 __builtin_clzll() 相反。 三元运算符编译为 cmove 指令。

【讨论】:

    猜你喜欢
    • 2022-01-21
    • 2014-02-16
    • 1970-01-01
    • 1970-01-01
    • 2014-07-14
    • 1970-01-01
    • 2022-12-03
    • 2022-01-15
    • 2019-08-12
    相关资源
    最近更新 更多