【问题标题】:How to count leading zeros in a 32 bit unsigned integer [closed]如何计算 32 位无符号整数中的前导零 [关闭]
【发布时间】:2014-07-14 10:17:27
【问题描述】:

谁能告诉我在 C 编程中计算 32 位无符号整数中前导零数量的有效算法是什么?

【问题讨论】:

  • 计算尾随的非0s 并从 32 位整数的最大可能位数中减去结果。
  • @alk 试试0...0101。 29 个前导零,1 个尾随 1,29 != 32-1。
  • 有什么问题? @德尔南?好的,我的建议措辞不准确。
  • 请参阅graphics.stanford.edu/~seander/bithacks.html 了解一系列技巧。在您的特定情况下,请记住前导零的数量和最左边的位置可以很容易地相互计算。
  • @delnan - 这完全取决于您对“最佳”的定义。

标签: c 32-bit unsigned-integer leading-zero


【解决方案1】:

本讨论假定您的编译器不支持该操作,或者它不能产生足够好的汇编。请注意,现在这两种情况都不太可能,因此我建议您仅在编译器上使用 __builtin_clz 用于 gcc 或等效项。

请注意,确定哪个是“最佳”clz 算法只能由您自己完成。现代处理器是复杂的野兽,这些算法的性能在很大程度上取决于你运行它的平台、你扔给它的数据以及使用它的代码。唯一确定的方法是测量,测量和测量更多。如果您无法区分,那么您可能没有关注您的瓶颈,您的时间将更好地花在其他地方。

现在无聊的免责声明已经结束,让我们看看Hacker's Delight 对这个问题有什么看法。一项快速调查表明,所有算法都依赖于对某些描述的二分搜索。这是一个简单的例子:

int n = 32;
unsigned y;

y = x >>16; if (y != 0) { n = n -16; x = y; }
y = x >> 8; if (y != 0) { n = n - 8; x = y; }
y = x >> 4; if (y != 0) { n = n - 4; x = y; }
y = x >> 2; if (y != 0) { n = n - 2; x = y; }
y = x >> 1; if (y != 0) return n - 2;
return n - x;

请注意,这适用于 32 个整数,如果需要,它也可以转换为迭代版本。不幸的是,该解决方案没有大量的指令级并行性,并且有相当多的分支,这并不能构成一个非常好的旋转算法。请注意,上面的代码存在一个无分支版本,但它更冗长,所以我不会在这里重现。

所以让我们通过使用 pop 指令(计算位数)来改进解决方案:

x = x | (x >> 1);
x = x | (x >> 2);
x = x | (x >> 4);
x = x | (x >> 8);
x = x | (x >>16);
return pop(~x);

那么这是如何工作的呢?关键是末尾的pop(~x) 指令,它计算x 中零的数量。为了使零的计数有意义,我们首先需要去掉所有不领先的 0。我们通过使用二进制算法正确传播 1 来做到这一点。虽然我们仍然没有太多的指令级并行性,但我们确实摆脱了所有分支,并且它使用的周期比之前的解决方案更少。好多了。

那么那个弹出指令怎么样,这不是作弊吗?大多数架构都有一个 1 周期的弹出指令,可以通过编译器内置指令(例如 gcc 的 __builtin_pop)访问。否则存在基于表的解决方案,但在权衡缓存访问周期时必须小心,即使表完全保存在 L1 缓存中。

最后,像通常的黑客一样,我们开始在陌生的领域徘徊。让我们用浮点数计算一些前导零:

union {
    unsigned asInt[2];
    double asDouble;
};
asDouble = (double)k + 0.5;
return 1054 - (asInt[LE] >> 20);

首先,一点警告:不要使用此算法。就标准而言,这会触发未定义的行为。这是为了有趣的因素而复制的,而不是任何实际用途。使用后果自负。

现在免责声明已经不存在了,它是如何工作的?它首先将 int 转换为 double 并继续提取 double 的指数分量。整洁的东西。如果在 little-endian 机器上执行 LE 常量应该是 1,在 big-endian 机器上执行应该是 0。

这应该让您简要了解解决此问题的各种位旋转算法。请注意,这本书有几种变体,它们会做出各种权衡,但我会让你自己发现这些。

【讨论】:

  • 有了这个,你可以忽略机器的字节序。 int clz(uint32_t x){联合{双ddd; int64_t uu; } 你; u.ddd = x + 0.5;返回 1054 - (int)(u.uu >> 52); }
  • 可悲的是,hackersdelight.org 似乎已不复存在,并且该域已被垃圾邮件发送者接管。谷歌搜索确实在网上找到了一些 pdf 副本。
【解决方案2】:

这可能是在纯 C 中执行此操作的最佳方式:

int clz(uint32_t x)
{
    static const char debruijn32[32] = {
        0, 31, 9, 30, 3, 8, 13, 29, 2, 5, 7, 21, 12, 24, 28, 19,
        1, 10, 4, 14, 6, 22, 25, 20, 11, 15, 23, 26, 16, 27, 17, 18
    };
    x |= x>>1;
    x |= x>>2;
    x |= x>>4;
    x |= x>>8;
    x |= x>>16;
    x++;
    return debruijn32[x*0x076be629>>27];
}

一个限制:正如所写,它不支持零输入(结果应该是 32)。如果您的所有输入都小于0x80000000,则可以通过将表中的第一个值更改为 32 来支持零而无需额外费用。否则,只需在开头添加一行:

    if (!x) return 32;

【讨论】:

  • 作为记录,Hacker's Delight 还包含此算法以及对其工作方式和原因的解释。我只是懒得复制整个表格:)
  • 我的桌子和他们的一样吗?我通过反转我用于 ctz 的表来手动生成它以代替 clz。
  • 实际上有 2 个。第一个是Harley's,它使用更大的表大小(64),没有增量并使用不同的乘数(0x06EB14F9)和移位操作(26)。第二个是 Goryavsky,他实际上推导出了几个变体,这些变体具有各种权衡(更小的表大小、更好的 ILP 等)。
  • 法律方面:您是否允许在商业软件中使用您的clz?询问是因为在 2018 年 5 月 2 日(UTC)或之后贡献的内容是根据 CC BY-SA 4.0 (link) 的条款分发的。 CC BY-SA 4.0 可能在商业/专有软件的许可证方面存在(兼容性)问题。如果是,那么在什么条件下?
【解决方案3】:

让我们计算不是前导零的位数。之后我们只做 (32 - n)。首先,如果数字为零,则 n 为零。否则:

n = 1 + floor(log2(x))

也就是说,我们使用以二为底的对数来找出最重要的非零位在哪个位置。我们可以使用计算 log2 的 FYL2X 指令在 x86 上高效地做到这一点。

但既然我们谈论的是 x86 指令,我们不妨看看真正可用的指令。这里是! http://en.wikipedia.org/wiki/Find_first_set - 你可以看到有很多指令可以直接做你想做的事情——如果你愿意编写汇编或者至少确认你的优化编译器会为你生成这些指令,给你一些精心编写的 C 代码。

【讨论】:

  • OP 专门要求在 C 中而不是在 x86 asm 中的最佳算法。
  • “高效”和“fyl2x”不能放在同一个句子中。这是迄今为止最慢的指令之一。
  • 当您可以在较新的架构上使用 bsrlzcnt 时,为什么还要选择这种神秘(而且速度很慢 - x87)的东西?
  • @BrettHale:我链接到关于 bsr 的维基百科页面。当然这是应该使用的。我在最后一段中讨论了这一点。
  • @John Zwinck,非常感谢您提供的所有信息。我们还可以使用 ceil(log2(x+1)) 来查找 x 中的二进制位数。正确的 ?现在,计算需要恒定的时间吗?如果是有符号整数,我应该怎么做才能计算给定输入的二进制位数?再次感谢您的合作:)
猜你喜欢
  • 2022-01-15
  • 1970-01-01
  • 2015-07-21
  • 2022-01-21
  • 1970-01-01
  • 1970-01-01
  • 2011-01-31
  • 2012-07-27
  • 2011-06-02
相关资源
最近更新 更多