请问有什么高效的算法可以在C编程中计算32位无符号整数前导零的数量?
本讨论假设你的编译器要么不支持该操作,要么无法生成足够好的汇编代码。请注意,这两种情况在现今都不太可能发生,因此我建议您只需在gcc或您的编译器上使用__builtin_clz
或等效函数。
请注意,确定哪种“最佳”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来实现这一点。虽然我们仍然没有太多的指令级并行性,但我们已经摆脱了所有的分支,并且使用的周期比之前的解决方案少。好得多。
那么这个pop指令怎么样,这不是欺骗吗?大多数架构都有一个1个周期的pop指令,可以通过编译器内置函数(例如gcc的__builtin_pop
)访问。否则,存在基于表格的解决方案,但必须注意在时间和缓存访问之间进行折衷,即使表格完全保存在L1缓存中也是如此。
最后,像往常一样,对于《黑客乐园》,我们开始漫步在奇怪的领域。让我们使用浮点数计算一些前导零:
union {
unsigned asInt[2];
double asDouble;
};
asDouble = (double)k + 0.5;
return 1054 - (asInt[LE] >> 20);
首先,有一个小警告:不要使用这个算法。就标准而言,这会触发未定义行为。这个算法只是为了好玩而复制而不是实际使用。使用需自担风险。
既然免责声明已经说完了,那么它是如何工作的呢?它首先将int转换为double,然后提取double的指数部分。很简洁的东西。如果在一个小端机器上执行,LE常量应该是1,如果在一个大端机器上执行,则为0。
这应该为你提供了各种位操作算法的简要概述。请注意,本书有几个这些算法的变体,其中有各种权衡,但我会让你自己去发现它们。
这可能是使用纯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;
让我们数一下不是前导零的数字的数量。之后我们只需要做 (32 - n)。首先,如果数字是零,那么 n 就是零。否则:
n = 1 + floor(log2(x))
也就是说,我们使用二进制对数来确定最高位非零位的位置。在x86上,我们可以使用FYL2X指令来高效地完成这个操作,该指令计算log2。
但既然我们正在谈论x86指令,我们不妨看看实际可用的内容。在这里!http://en.wikipedia.org/wiki/Find_first_set - 你可以看到有很多直接执行所需操作的指令 - 如果你愿意编写汇编代码或至少确认你的优化编译器在给定一些精心编写的C代码后生成这些指令。
bsr
或lzcnt
呢? - Brett Hale
longlong.h
中执行此操作的代码。它由2200行代码组成,具有多个分支,取决于CPU及其特性。该文件没有使用一行内联ASM。 - jww__asm__
。 - pmor