如何有效地计算 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&lt;&lt;24 溢出,for 已更改为for (unsigned long long i = 0; i &lt;= 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 位无符号整数中的前导零?的主要内容,如果未能解决你的问题,请参考以下文章

C语言里怎样理解长整型 短整型 和无符号型变量和常量?

如何从 32 位 R 整数中提取 4 位无符号整数?

c语言编程将16位无符号数的高8位和低8位交换.

如何在 AVX2 中将 32 位无符号整数转换为 16 位无符号整数?

如何在 C 中提取 32 位无符号整数的特定“n”位?

delphi 中byte类型