如何有效地计算 24 位无符号整数中的前导零?
Posted
技术标签:
【中文标题】如何有效地计算 24 位无符号整数中的前导零?【英文标题】:How to efficiently count leading zeros in a 24 bit unsigned integer? 【发布时间】:2022-01-15 08:40:03 【问题描述】:大部分clz()
(SW impl.)是optimized for 32 bit unsigned integer。
如何有效计算 24 位无符号整数中的前导零?
UPD。目标特征:
CHAR_BIT 24
sizeof(int) 1
sizeof(long int) 2
sizeof(long long int) 3
【问题讨论】:
您是在谈论仅包含 24 位的真实数据类型,还是在谈论在本机 32 位类型中使用 24 位? 你的 24 位无符号整数是如何存储的?在 32 位 int 中,或类似uint8_t[3]
?
@Ben endian 在这种情况下并不重要。并且领先意味着从最高到最低位。
@Fredrik 当然很重要。这是一个位旋转练习,所以你需要知道位在内存中是如何布局的。
@Ben 不,你不知道。仅当您将多部分字节类型分解为较小的字节类型时,Endian 才重要,例如将 int 指针转换为 char 指针。对类型使用按位运算不关心字节序。
【参考方案1】:
TL;DR:C 程序见下文第 4 点。
假设您的假设目标机器能够正确实现无符号 24 位乘法(必须返回乘积的低 24 位),您可以使用与您链接的答案中显示的相同的技巧。 (但您可能不想这样做。请参阅 [注 1]。)值得尝试了解链接答案中发生的情况。
输入被缩减为一小组值,其中具有相同数量前导零的所有整数映射到相同的值。这样做的简单方法是淹没每个位以覆盖它右侧的所有位位置:
x |= x>>1;
x |= x>>2;
x |= x>>4;
x |= x>>8;
x |= x>>16;
这适用于 17 到 32 位;如果您的目标数据类型有 9 到 16 位,您可以省略最后一个移位和或,因为在任何位的右侧没有 16 位的位位置。等等。但是对于 24 位,您将需要所有五个移位和或。
这样,您已将 x 转换为 25 个值之一(对于 24 位整数):
x clz x clz x clz x clz x clz
-------- --- -------- --- -------- --- -------- --- -------- ---
0x000000 24 0x00001f 19 0x0003ff 14 0x007fff 9 0x0fffff 4
0x000001 23 0x00003f 18 0x0007ff 13 0x00ffff 8 0x1fffff 3
0x000003 22 0x00007f 17 0x000fff 12 0x01ffff 7 0x3fffff 2
0x000007 21 0x0000ff 16 0x001fff 11 0x03ffff 6 0x7fffff 1
0x00000f 20 0x0001ff 15 0x003fff 10 0x07ffff 5 0xffffff 0
现在,要将 x 转换为 clz,我们需要一个好的散列函数。我们不一定期望 hash(x)==clz,但我们希望 25 个可能的 x 值散列为不同的数字,理想情况下在一个小范围内。与您提供的链接一样,我们将选择的散列函数是乘以精心选择的被乘数,然后屏蔽掉一些位。使用掩码意味着我们需要选择五个位;理论上,我们可以在 24 位字的任意位置使用 5 位掩码,但为了不用想太多,我只选择了高位 5 位,与 32 位方案相同。与 32 位解决方案不同,我没有费心加 1,并且我希望为所有 25 个可能的输入区分不同的值。使用 5 位掩码和 33 个可能的 clz 值(如在 32 位情况下)是不可能的,因此如果原始输入为 0,它们必须跳过一个额外的环。
由于哈希函数不直接产生 clz 值,而是 0 到 31 之间的数字,因此我们需要将结果转换为 clz 值,该值使用 32 字节查找表,称为debruijn
in 32 位算法,原因我不打算讨论。
一个有趣的问题是如何选择具有所需特性的乘数。一种可能性是做一堆数论来优雅地发现一个解决方案。几十年前就是这样做的,但现在我可以编写一个快速而简单的 Python 程序来对所有可能的乘数进行暴力搜索。毕竟,在 24 位的情况下,只有大约 1600 万种可能性,而且其中很多都有效。我使用的实际 Python 代码是:
# Compute the 25 target values
targ=[2**i - 1 for i in range(25)]
# For each possible multiplier, compute all 25 hashes, and see if they
# are all different (that is, the set of results has size 25):
next(i for i in range(2**19, 2**24)
if len(targ)==len(set(((i * t) >> 19) & 0x1f
for t in targ)))
在生成器表达式上调用next
返回第一个生成值,在本例中为 0x8CB4F 或 576335。由于搜索从 0x80000 开始(这是 hash(1) 不为 0 的最小乘数),结果立即打印出来。然后我又花了几毫秒的时间生成了 219 和 220 之间所有可能的乘数,其中有 90 个,并选择了 0xCAE8F (831119) 纯粹出于个人审美原因。
最后一步是从计算的哈希函数创建查找表。 (并不是说这是好的 Python。我只是从我的命令历史记录中取出它;我可能稍后会回来清理它。但为了完整起见,我将它包括在内。):
lut = dict((i,-1) for i in range(32))
lut.update((((v * 0xcae8f) >> 19) & 0x1f, 24 - i)
for i, v in enumerate(targ))
print(" static const char lut[] = \n " +
",\n ".join(', '.join(f"lut[i]:2" for i in range(j, j+8))
for j in range(0, 32, 8)) +
"\n ;\n")
# The result is pasted into the C code below.
那么这只是汇编 C 代码的问题:
// Assumes that `unsigned int` has 24 value bits.
int clz(unsigned x)
static const char lut[] =
24, 23, 7, 18, 22, 6, -1, 9,
-1, 17, 15, 21, 13, 5, 1, -1,
8, 19, 10, -1, 16, 14, 2, 20,
11, -1, 3, 12, 4, -1, 0, -1
;
x |= x>>1;
x |= x>>2;
x |= x>>4;
x |= x>>8;
x |= x>>16;
return lut[((x * 0xcae8f) >> 19) & 0x1f];
测试代码在每个 24 位整数上依次调用 clz
。由于我手边没有 24 位机器,我只是假设算术在 OP 中假设的 24 位机器上也能正常工作。
#include <stdio.h>
# For each 24-bit integer in turn (from 0 to 2**24-1), if
# clz(i) is different from clz(i-1), print clz(i) and i.
#
# Expected output is 0 and the powers of 2 up to 2**23, with
# descending clz values from 24 to 0.
int main(void)
int prev = -1;
for (unsigned i = 0; i < 1<<24; ++i)
int pfxlen = clz(i);
if (pfxlen != prev)
printf("%2d 0x%06X\n", pfxlen, i);
prev = pfxlen;
return 0;
注意事项:
如果目标机器没有在硬件中实现 24 位无符号乘法——也就是说,它依赖于软件仿真——那么通过循环初始位来执行 clz 几乎肯定会更快,特别是如果你折叠通过使用查找表一次扫描几个位来循环。即使机器确实进行了高效的硬件倍增,这也可能会更快。例如,您可以使用 32 项表一次扫描 6 位:
// Assumes that `unsigned int` has 24 value bits.
int clz(unsigned int x)
static const char lut[] =
5, 4, 3, 3, 2, 2, 2, 2,
1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0
;
/* Six bits at a time makes octal easier */
if (x & 077000000u) return lut[x >> 19];
if (x & 0770000u) return lut[x >> 13] + 6;
if (x & 07700u) return lut[x >> 7] + 12;
if (x ) return lut[x >> 1] + 18;
return 24;
该表可以减少到 48 位,但额外的代码可能会消耗掉节省的空间。
这里似乎需要进行一些澄清。首先,虽然我们一次扫描六位,但我们只使用其中五位来索引表。那是因为我们之前已经验证了所讨论的六个位并非全为零。在这种情况下,低位要么不相关(如果设置了其他位),要么为 1。此外,我们通过不加掩码的移位获得表索引;屏蔽是不必要的,因为我们从屏蔽测试中知道所有高阶位都是 0。(但是,如果 x
超过 24 位,这将失败。)
【讨论】:
确认:在 24 位机器的模拟器上,测试代码产生预期的输出。由于1<<24
溢出,for
已更改为for (unsigned long long i = 0; i <= 0xffffff; ++i)
。
法律方面:您是否允许在商业软件中使用您的clz
?询问是因为在 2018 年 5 月 2 日(UTC)或之后贡献的内容是根据 CC BY-SA 4.0 (link) 的条款分发的。 CC BY-SA 4.0 可能在商业/专有软件的许可方面存在(兼容性)问题。
如果是,那么在什么条件下?
@pmor 我不主张该答案的知识产权。随意使用。
多么棒的答案!【参考方案2】:
将 24 位整数转换为 32 位整数(通过类型双关语或显式混排位),然后转换为 32 位 clz,然后减去 8。
为什么要那样做?因为在当今时代,您将很难找到一台原生处理 24 位类型的机器。
【讨论】:
问题表明它与软件实现有关,因此硬件 clz 无关紧要。有软件解决方案,但 OP 正在寻找一些可以“修剪”处理高 8 位的代码部分或以其他方式优化 24 位而不是 32 位的东西。所以这个答案没有提供任何相关性。此外,不需要双关语或“洗牌”位;如果需要,对uint32_t
的简单转换将产生填充到 32 位的值。
@EricPostpischil 如果 24 位值存储在 3 字节字节数组中(例如来自传感器的数据),则很难转换。
@0___________:那么你仍然无法将其键入 32 位整数(使用新类型对访问进行类型双关,因此它将加载 32 位),并且“位改组”无关紧要.
@EricPostpischil 你可以记忆它(假设相同的字节序)
@Ben 澄清一下,sizeof(long long int)
返回 3 和 CHAR_BIT = 24
,意味着 long long int
是 72 位。【参考方案3】:
我会寻找可用于您的平台和编译器的内置函数或内部函数。这些函数通常实现查找最高有效位数的最有效方法。例如,gcc 有 __builtin_clz 函数。
如果 24 位整数存储在字节数组中(例如从传感器接收)
#define BITS(x) (CHAR_BIT * sizeof(x) - 24)
int unaligned24clz(const void * restrict val)
unsigned u = 0;
memcpy(&u, val, 3);
#if defined(__GNUC__)
return __builtin_clz(u) - BITS(u);
#elif defined(__ICCARM__)
return __CLZ(u) - BITS(u);
#elif defined(__arm__)
return __clz(u) - BITS(u);
#else
return clz(u) - BITS(u); //portable version using standard C features
#endif
如果存储为有效整数
int clz24(const unsigned u)
#if defined(__GNUC__)
return __builtin_clz(u) - BITS(u);
#elif defined(__ICCARM__)
return __CLZ(u) - BITS(u);
#elif defined(__arm__)
return __clz(u) - BITS(u);
#else
return clz(u) - BITS(u); //portable version using standard C features
#endif
https://godbolt.org/z/z6n1rKjba
如果需要,您可以添加更多编译器支持。
请记住,如果值为 0
,则 __builtin_clz
的值未定义,因此您需要添加另一个检查。
【讨论】:
以上是关于如何有效地计算 24 位无符号整数中的前导零?的主要内容,如果未能解决你的问题,请参考以下文章