【问题标题】:Compute fast log base 2 ceiling计算快速对数基数 2 上限
【发布时间】:2010-07-17 16:59:05
【问题描述】:

什么是计算(long int) ceiling(log_2(i)) 的快速方法,其中输入和输出是 64 位整数?有符号或无符号整数的解决方案是可以接受的。我怀疑最好的方法是一种类似于here 的方法,但与其尝试我自己的方法,我更愿意使用已经过良好测试的方法。通用解决方案适用于所有正值。

例如,2,3,4,5,6,7,8 的值为 1,2,2,3,3,3,3

编辑:到目前为止,最好的方法似乎是使用任意数量的快速现有 bithacks 或寄存器方法来计算整数/地板对数基数 2(MSB 的位置),然后如果输入不是,则添加一个两个的幂。对 2 的幂的快速按位检查是 (n&(n-1))

编辑 2:关于整数对数和前导零方法的一个很好的来源是 Henry S. Warren 在Hacker's Delight 中的第 5-3 和 11-4 节。这是我发现的最完整的治疗方法。

编辑 3:这种技术看起来很有前途:https://stackoverflow.com/a/51351885/365478

【问题讨论】:

  • 至少对于严格大于一且小于一个大数(例如 2^63 或 2^62)的所有值都必须完全正确。
  • 请看下面我的回答。我放了一个解释+将为您执行此操作的代码。
  • 过去,我通常使用查找表和位旋转的组合,类似于位旋转页面的链接。
  • 如果您只处理正值,处理舍入的一种简单方法是找到为((x << 1) - 1) 设置的最高有效位。你需要特例x == 0,如果设置了最高位,你会溢出,但这种方法可能比其他一些舍入技术更快。
  • 在 C++20 中只使用std::bit_ceil,不幸的是这个问题是关于 C

标签: c optimization math 64-bit bit-manipulation


【解决方案1】:

这个算法已经发布了,但是下面的实现非常紧凑,应该优化成无分支代码。

int ceil_log2(unsigned long long x)
{
  static const unsigned long long t[6] = {
    0xFFFFFFFF00000000ull,
    0x00000000FFFF0000ull,
    0x000000000000FF00ull,
    0x00000000000000F0ull,
    0x000000000000000Cull,
    0x0000000000000002ull
  };

  int y = (((x & (x - 1)) == 0) ? 0 : 1);
  int j = 32;
  int i;

  for (i = 0; i < 6; i++) {
    int k = (((x & t[i]) == 0) ? 0 : j);
    y += k;
    x >>= k;
    j >>= 1;
  }

  return y;
}


#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
  printf("%d\n", ceil_log2(atol(argv[1])));

  return 0;
}

【讨论】:

  • 我认为这是最佳答案。对于任何关注讨论的人来说,这是目前最佳答案的原因是汇编语言解决方案是特定于平台的。
  • 是否有机会重写它以使用信息变量名称,以便了解它是如何工作的?
  • 你为什么用int而不是unsigned
  • @sebastian 答案总是在 0 到 64 之间,因此可以使用任何整数类型。而且由于可以使用任何整数类型,所以我选择了默认类型“int”。
【解决方案2】:

如果您可以限制自己使用 gcc,则有一组内置函数可以返回前导零位的数量,并且可以通过一些工作来做您想做的事情:

int __builtin_clz (unsigned int x)
int __builtin_clzl (unsigned long)
int __builtin_clzll (unsigned long long)

【讨论】:

    【解决方案3】:

    如果您在 Windows 上为 64 位处理器进行编译,我认为这应该可以。 _BitScanReverse64 是一个内在函数。

    #include <intrin.h>
    __int64 log2ceil( __int64 x )
    {
      unsigned long index;
      if ( !_BitScanReverse64( &index, x ) )
         return -1LL; //dummy return value for x==0
    
      // add 1 if x is NOT a power of 2 (to do the ceil)
      return index + (x&(x-1)?1:0);
    }
    

    对于 32 位,您可以模拟 _BitScanReverse64,对 _BitScanReverse 进行 1 或 2 次调用。 检查 x 的高 32 位 ((long*)&x)[1],如果需要,检查低 32 位 ((long*)&x)[0]。

    【讨论】:

    • 我在想一些比 Windows 特定的更通用的东西,但到目前为止,你的想法是最接近答案的。翻译后的想法是,有一种快速的按位方法来检查某事物是否是 2 的幂(如上所示)。我们可以将此方法与寄存器方法结合使用,以确定 MSB 的位置来检索答案。
    • +1 和@highperformance:如果您只打算在 x86 处理器上运行您的代码,您可以通过一点组装自己进行位扫描。 (具体是BSR 指令)
    • @casablanca:使用编译器理解的内在函数比使用实际的内联汇编要好得多。编译器可以通过 _BitScanReverse64 / __builtin_clzll 进行常量传播,但不能通过 inline-asm 语句。另外,MSVC inline asm is total garbage for wrapping a single instruction。 (GNU C 内联汇编确实可以,但仍然没有常量传播)。此外,__builtin_clzll 是可移植的 GNU C,并且可以在任何目标机器上编译成任何好的东西。
    【解决方案4】:

    我所知道的最快的方法是使用快速向下舍入的log2,结合无条件调整前后输入值来处理向上舍入的情况,如下所示的lg_down()

    /* base-2 logarithm, rounding down */
    static inline uint64_t lg_down(uint64_t x) {
      return 63U - __builtin_clzl(x);
    }
    
    /* base-2 logarithm, rounding up */
    static inline uint64_t lg_up(uint64_t x) {
      return lg_down(x - 1) + 1;
    }
    

    基本上将 1 添加到舍入结果对于除 2 的精确幂之外的所有值都是正确的(因为在这种情况下,floorceil 方法应该返回相同的答案),因此减去就足够了1 从输入值处理该情况(它不会更改其他情况的答案)并将结果加一。

    这通常比通过明确检查 2 的确切幂来调整值的方法稍微快一些(例如,添加 !!(x &amp; (x - 1)) 术语)。它避免了任何比较和条件操作或分支,内联时更有可能简单,更适合向量化等。

    这依赖于大多数 CPU 使用内置 clang/icc/gcc __builtin_clzl 提供的“计数前导位”功能,但其他平台也提供类似的功能(例如,Visual Studio 中的 BitScanReverse 内在函数)。

    不幸的是,很多人都返回了log(1) 的错误答案,因为这会导致__builtin_clzl(0) 这是基于 gcc 文档的未定义行为。当然,一般的“计数前导零”函数在零处具有完美定义的行为,但 gcc 内置函数是以这种方式定义的,因为在 x86 上的 BMI ISA 扩展之前,它会使用本身未定义的 bsr instruction行为。

    如果您知道自己有 lzcnt 指令,则可以通过直接使用 lzcnt 内在函数来解决此问题。除 x86 之外的大多数平台一开始就从未遇到过 bsr 错误,并且可能还提供了访问其“计数前导零”指令的方法(如果有的话)。

    【讨论】:

    • 这看起来很有希望,因为我已经编辑了主要问题以表明。您是否在相关案例和角落(例如 0、1 和 2)、完整的 32 位(无符号)整数空间、对数输出中每一步变化的三个整数以及 uint64_t max 附近的值进行了测试?
    • @kevinlawler - 我使用 32 位版本,并在那里测试了所有 32-but 值。但是,关于 __builtin_clzl 有一个警告 - 传递零是未定义的,这意味着 log(1) 是未定义的。实际上,当 clz 在 x86 或 ARM 等效指令上编译为 lzcnt 指令时,它可以工作,但如果它以 bsr 结尾则不能。考虑到 GCC 内在的这种行为,也许有一些方法可以定义它但仍然很快。如果您知道您正在使用 lzcnt 的平台上进行编译,则可以使用内部函数。我将用这些警告更新答案。
    • @BeeOnRope 很好的尝试,但是唉!,我下面的答案在 x86-64 中要好 33%(三个汇编指令的复杂度与你的四个相当),请参阅here at the compiler explorer 的区别。我不确定为什么 GCC 和 Clang(顺便说一下,这会产生更好的结果)会惩罚你的解决方案
    • @TheCppZoo - 在没有指定架构的特定情况下,是的。但是,我通常针对具有lzcnt 的机器,因此使用-march=haswell 它们是more comparable - 至少在clang、gcc 和icc 上,此答案中的解决方案是与您的指令数量相同(4),并且可以说在实践中稍微好一点,因为它避免了在较少端口上运行的lea,尤其是 gcc 和 icc(但不是 clang)使用的“3 组件 lea”,因为它在 1 个端口上运行并具有 3 个延迟周期。跨度>
    • 总而言之,这可能有点学术性:真正关心的用户可以测试两者,并且由于这个函数很可能被内联(如果性能很重要,应该是内联),代码如何在内联最终变得更重要之后才起作用。
    【解决方案5】:
    #include "stdafx.h"
    #include "assert.h"
    
    int getpos(unsigned __int64 value)
    {
        if (!value)
        {
          return -1; // no bits set
        }
        int pos = 0;
        if (value & (value - 1ULL))
        {
          pos = 1;
        }
        if (value & 0xFFFFFFFF00000000ULL)
        {
          pos += 32;
          value = value >> 32;
        }
        if (value & 0x00000000FFFF0000ULL)
        {
          pos += 16;
          value = value >> 16;
        }
        if (value & 0x000000000000FF00ULL)
        {
          pos += 8;
          value = value >> 8;
        }
        if (value & 0x00000000000000F0ULL)
        {
          pos += 4;
          value = value >> 4;
        }
        if (value & 0x000000000000000CULL)
        {
          pos += 2;
          value = value >> 2;
        }
        if (value & 0x0000000000000002ULL)
        {
          pos += 1;
          value = value >> 1;
        }
        return pos;
    }
    
    int _tmain(int argc, _TCHAR* argv[])
    {    
        assert(getpos(0ULL) == -1); // None bits set, return -1.
        assert(getpos(1ULL) == 0);
        assert(getpos(2ULL) == 1);
        assert(getpos(3ULL) == 2);
        assert(getpos(4ULL) == 2);
        for (int k = 0; k < 64; ++k)
        {
            int pos = getpos(1ULL << k);
            assert(pos == k);
        }
        for (int k = 0; k < 64; ++k)
        {
            int pos = getpos( (1ULL << k) - 1);
            assert(pos == (k < 2 ? k - 1 : k) );
        }
        for (int k = 0; k < 64; ++k)
        {
            int pos = getpos( (1ULL << k) | 1);
            assert(pos == (k < 1 ? k : k + 1) );
        }
        for (int k = 0; k < 64; ++k)
        {
            int pos = getpos( (1ULL << k) + 1);
            assert(pos == k + 1);
        }
        return 0;
    }
    

    【讨论】:

    • 查找表将消除最后几个if 子句,其中四个或更多取决于可用内存。
    • 要使用 16 位查找表:声明全局 short getpos_lookup[1 &lt;&lt; 16];。预填充:@​​987654324@ 然后在16 案例下注释掉8/4/2/1 案例并放入pos += getpos_lookup[v];
    • 实际上我什至不确定我的版本是否很快。使用 for 循环可能比任何其他方法都快。
    • 我进行了一些简单的测试。您的方法比循环快得多(例如while(value&gt;&gt;=1)pos++;)。修改您的方法以使用查找表稍微快一些,但我不会说它明显更快。出于我的目的,您的方法已经足够快了。但是,如果有人希望继续改进它,我会考虑: 1. 用寄存器调用替换 MSB 检测(可能使用 #ifdef 语句来实现可移植性)。 2. 采用一些启发式方法来利用输入的已知分布(例如,90% 的传入数字低于 1000) 3. 使用查找表
    • 通过切换到像0xFFFF0000FFFF0000, 0xFF00FF00FF00FF00, ... 这样的常量,你可以移除这些变化。
    【解决方案6】:

    使用@egosys 提到的 gcc 内置函数,您可以构建一些有用的宏。 对于快速粗略的 floor(log2(x)) 计算,您可以使用:

    #define FAST_LOG2(x) (sizeof(unsigned long)*8 - 1 - __builtin_clzl((unsigned long)(x)))
    

    对于类似的 ceil(log2(x)),使用:

    #define FAST_LOG2_UP(x) (((x) - (1 << FAST_LOG2(x))) ? FAST_LOG2(x) + 1 : FAST_LOG2(x))
    

    后者可以使用更多 gcc 特性进一步优化以避免对内置函数的双重调用,但我不确定你是否需要它。

    【讨论】:

    • 谢谢。我对此的担忧是对内置函数的调用。它确实在 gcc 和 clang 上编译,这将涵盖大多数实例。如果我知道它是在 icc 上编译的,我可能会选择它。跨平台兼容性是一个问题。我也不介意按照您的建议清理双重通话。 (可以不用(x&amp;(x-1))吗?)
    • 您可以使用#define FAST_LOG2_UP(x) ({ unsigned long log = FAST_LOG2(x); ((x) - (1 &lt;&lt; log)) ? log + 1 : log; }) 来避免多次调用内置函数。请注意,这又是 gcc 特定的。
    • 另一个改进可以是消除分支:log + !!(x ^ (1
    • 在我的测试中,AND-MINUS 方法的两个变体 ((x&(x-1))?1:0) 和 (!!(x&(x-1))) 在 1 内运行彼此的百分比:可能相同。 XOR-SHIFT 方法 MSB+!!(x^(1
    【解决方案7】:

    以下代码 sn-p 是一种安全且可移植的方式,用于扩展纯 C 方法(例如 @dgobbi 的方法),以便在使用支持编译器 (Clang) 进行编译时使用编译器内在函数。将它放在方法的顶部将导致该方法在可用时使用内置函数。当内置函数不可用时,该方法将回退到标准 C 代码。

    #ifndef __has_builtin
    #define __has_builtin(x) 0
    #endif
    
    #if __has_builtin(__builtin_clzll) //use compiler if possible
      return ((sizeof(unsigned long long) * 8 - 1) - __builtin_clzll(x)) + (!!(x & (x - 1)));
    #endif
    

    【讨论】:

      【解决方案8】:

      真正最快的解决方案:

      包含 63 个条目的二叉搜索树。这些是从 0 到 63 的 2 的幂。创建树的一次性生成函数。叶子代表幂的以 2 为底的对数(基本上是数字 1-63)。

      要找到答案,您将一个数字输入树中,然后导航到大于该项目的叶节点。如果叶节点完全相等,则结果是叶值。否则,结果为叶子值 + 1。

      复杂度固定为 O(6)。

      【讨论】:

      • 当然你并不真的需要一棵树,只需要适应求根的二分法,使每次迭代的间隔减半。
      • 戴夫:谢谢。格雷格:当然。不过,对于初学者来说,树更容易可视化。
      • 在实践中,我认为这种分支在 CPU 上会变慢(基准测试会很有趣!)。更快是可能的,因为我们可以使用在 64 位上并行运行的指令。无论如何,您的方法就像链接中“在 O(lg(N)) 操作中查找 N 位整数的对数基数 2”的展开版本。并且... O(6)=O(1)=O(99999)
      • 二分查找不需要任何分支。它可以完全通过符号位/进位标志的算术来完成。
      【解决方案9】:

      查找具有整数输出的整数(64 位或任何其他位)的以 2 为底的对数等效于查找已设置的最高有效位。为什么?因为 log base 2 是你可以将数字除以 2 达到 1 的次数。

      找到设置的 MSB 的一种方法是每次简单地向右位移 1 直到得到 0。另一种更有效的方法是通过位掩码进行某种二进制搜索。

      通过检查是否设置了除 MSB 之外的任何其他位,可以轻松计算出 ceil 部分。

      【讨论】:

      • 你的意思不是每次向右位移 1 位,直到你有 1 吗?既然他想要天花板,那就是那个数字 + 1。
      • @Computer Guru:不,不是“那个数字+1”。它是“那个数字 + 1 如果结果不准确”。
      • 是的,而且很有可能它不会准确。所以“那个数字+1”比“那个数字”更准确:)
      • 好吧,这样呢:计算“1”位数,如果“1”位数大于1,则和1。
      • 我一般都知道该怎么做。困难在于找出哪一组特定的按位操作是最好的。我希望有一个类似于斯坦福 bithacks 页面上的现有解决方案。
      【解决方案10】:

      下面的代码更简单,只要输入 x >= 1 就可以工作。输入 clog2(0) 将得到未定义的答案(这是有道理的,因为 log(0) 是无穷大...)您可以添加错误如果需要,检查 (x == 0):

      unsigned int clog2 (unsigned int x)
      {
          unsigned int result = 0;
          --x;
          while (x > 0) {
              ++result;
              x >>= 1;
          }
      
          return result;
      }
      

      顺便说一句,log2的楼层代码类似:(再次假设x >= 1)

      unsigned int flog2 (unsigned int x)
      {
          unsigned int result = 0;
          while (x > 1) {
              ++result;
              x >>= 1;
          }
      
          return result;
      }
      

      【讨论】:

        【解决方案11】:

        在撰写本文时,我将为您提供 x86-64 的最快方法,如果您有一个 适用于参数 的快速地板,如果您喜欢的话,我会给您一个通用技术完整的范围,然后见下文。

        我对其他答案的低质量感到惊讶,因为它们告诉您如何获得地板,但以非常昂贵的方式(使用条件和一切!)将地板转换为天花板。

        如果你可以快速得到对数的底,例如使用__builtin_clzll,那么底很容易得到,如下所示:

        unsigned long long log2Floor(unsigned long long x) {
            return 63 - __builtin_clzll(x);
        }
        
        unsigned long long log2Ceiling(unsigned long long x) {
            return log2Floor(2*x - 1);
        }
        

        之所以有效,是因为它会将结果加 1,除非 x 正好是 2 的幂

        查看 x86-64 汇编器差异 at the compiler explorer 以了解像这样的天花板的另一种实现:

        auto log2CeilingDumb(unsigned long x) {
            return log2Floor(x) + (!!(x & (x - 1)));
        }
        

        给予:

        log2Floor(unsigned long): # @log2Floor(unsigned long)
          bsr rax, rdi
          ret
        log2CeilingDumb(unsigned long): # @log2CeilingDumb(unsigned long)
          bsr rax, rdi
          lea rcx, [rdi - 1]
          and rcx, rdi
          cmp rcx, 1
          sbb eax, -1
          ret
        log2Ceiling(unsigned long): # @log2Ceiling(unsigned long)
          lea rax, [rdi + rdi]
          add rax, -1
          bsr rax, rax
          ret
        

        对于完整的范围,它在之前的答案中:return log2Floor(x - 1) + 1,这要慢得多,因为它在 x86-64 中使用了四个指令,而不是上面的三个。

        【讨论】:

        • 我认为您对其他答案的“低质量”的描述“因为他们告诉您如何获得地板但以非常昂贵的价格改造地板”是不合理的,因为其他答案包括my answer 与您的基本相同(您只需将一个添加替换为 1* 2)。额外的指令或多或少只是您选择的编译器上的一个奇怪的编译怪癖。如果您改用 gcc,则需要 6 instructions
        • 一般来说,我希望生成的代码在各种编译器和硬件中具有可比性,当然除了这个版本在输入范围的一半返回错误的答案,我的表现出未定义的行为输入“1”(但在使用lzcnt 或类似名称时返回正确答案)。
        【解决方案12】:

        我已经对 64 位“最高位”的几个实现进行了基准测试。最“无分支”的代码实际上并不是最快的。

        这是我的highest-bit.c源文件:

        int highest_bit_unrolled(unsigned long long n)
        {
          if (n & 0xFFFFFFFF00000000) {
            if (n & 0xFFFF000000000000) {
              if (n & 0xFF00000000000000) {
                if (n & 0xF000000000000000) {
                  if (n & 0xC000000000000000)
                    return (n & 0x8000000000000000) ? 64 : 63;
                  else
                    return (n & 0x2000000000000000) ? 62 : 61;
                } else {
                  if (n & 0x0C00000000000000)
                    return (n & 0x0800000000000000) ? 60 : 59;
                  else
                    return (n & 0x0200000000000000) ? 58 : 57;
                }
              } else {
                if (n & 0x00F0000000000000) {
                  if (n & 0x00C0000000000000)
                    return (n & 0x0080000000000000) ? 56 : 55;
                  else
                    return (n & 0x0020000000000000) ? 54 : 53;
                } else {
                  if (n & 0x000C000000000000)
                    return (n & 0x0008000000000000) ? 52 : 51;
                  else
                    return (n & 0x0002000000000000) ? 50 : 49;
                }
              }
            } else {
              if (n & 0x0000FF0000000000) {
                if (n & 0x0000F00000000000) {
                  if (n & 0x0000C00000000000)
                    return (n & 0x0000800000000000) ? 48 : 47;
                  else
                    return (n & 0x0000200000000000) ? 46 : 45;
                } else {
                  if (n & 0x00000C0000000000)
                    return (n & 0x0000080000000000) ? 44 : 43;
                  else
                    return (n & 0x0000020000000000) ? 42 : 41;
                }
              } else {
                if (n & 0x000000F000000000) {
                  if (n & 0x000000C000000000)
                    return (n & 0x0000008000000000) ? 40 : 39;
                  else
                    return (n & 0x0000002000000000) ? 38 : 37;
                } else {
                  if (n & 0x0000000C00000000)
                    return (n & 0x0000000800000000) ? 36 : 35;
                  else
                    return (n & 0x0000000200000000) ? 34 : 33;
                }
              }
            }
          } else {
            if (n & 0x00000000FFFF0000) {
              if (n & 0x00000000FF000000) {
                if (n & 0x00000000F0000000) {
                  if (n & 0x00000000C0000000)
                    return (n & 0x0000000080000000) ? 32 : 31;
                  else
                    return (n & 0x0000000020000000) ? 30 : 29;
                } else {
                  if (n & 0x000000000C000000)
                    return (n & 0x0000000008000000) ? 28 : 27;
                  else
                    return (n & 0x0000000002000000) ? 26 : 25;
                }
              } else {
                if (n & 0x0000000000F00000) {
                  if (n & 0x0000000000C00000)
                    return (n & 0x0000000000800000) ? 24 : 23;
                  else
                    return (n & 0x0000000000200000) ? 22 : 21;
                } else {
                  if (n & 0x00000000000C0000)
                    return (n & 0x0000000000080000) ? 20 : 19;
                  else
                    return (n & 0x0000000000020000) ? 18 : 17;
                }
              }
            } else {
              if (n & 0x000000000000FF00) {
                if (n & 0x000000000000F000) {
                  if (n & 0x000000000000C000)
                    return (n & 0x0000000000008000) ? 16 : 15;
                  else
                    return (n & 0x0000000000002000) ? 14 : 13;
                } else {
                  if (n & 0x0000000000000C00)
                    return (n & 0x0000000000000800) ? 12 : 11;
                  else
                    return (n & 0x0000000000000200) ? 10 : 9;
                }
              } else {
                if (n & 0x00000000000000F0) {
                  if (n & 0x00000000000000C0)
                    return (n & 0x0000000000000080) ? 8 : 7;
                  else
                    return (n & 0x0000000000000020) ? 6 : 5;
                } else {
                  if (n & 0x000000000000000C)
                    return (n & 0x0000000000000008) ? 4 : 3;
                  else
                    return (n & 0x0000000000000002) ? 2 : (n ? 1 : 0);
                }
              }
            }
          }
        }
        
        int highest_bit_bs(unsigned long long n)
        {
          const unsigned long long mask[] = {
            0x000000007FFFFFFF,
            0x000000000000FFFF,
            0x00000000000000FF,
            0x000000000000000F,
            0x0000000000000003,
            0x0000000000000001
          };
          int hi = 64;
          int lo = 0;
          int i = 0;
        
          if (n == 0)
            return 0;
        
          for (i = 0; i < sizeof mask / sizeof mask[0]; i++) {
            int mi = lo + (hi - lo) / 2;
        
            if ((n >> mi) != 0)
              lo = mi;
            else if ((n & (mask[i] << lo)) != 0)
              hi = mi;
          }
        
          return lo + 1;
        }
        
        int highest_bit_shift(unsigned long long n)
        {
          int i = 0;
          for (; n; n >>= 1, i++)
            ; /* empty */
          return i;
        }
        
        static int count_ones(unsigned long long d)
        {
          d = ((d & 0xAAAAAAAAAAAAAAAA) >>  1) + (d & 0x5555555555555555);
          d = ((d & 0xCCCCCCCCCCCCCCCC) >>  2) + (d & 0x3333333333333333);
          d = ((d & 0xF0F0F0F0F0F0F0F0) >>  4) + (d & 0x0F0F0F0F0F0F0F0F);
          d = ((d & 0xFF00FF00FF00FF00) >>  8) + (d & 0x00FF00FF00FF00FF);
          d = ((d & 0xFFFF0000FFFF0000) >> 16) + (d & 0x0000FFFF0000FFFF);
          d = ((d & 0xFFFFFFFF00000000) >> 32) + (d & 0x00000000FFFFFFFF);
          return d;
        }
        
        int highest_bit_parallel(unsigned long long n)
        {
          n |= n >> 1;
          n |= n >> 2;
          n |= n >> 4;
          n |= n >> 8;
          n |= n >> 16;
          n |= n >> 32;
          return count_ones(n);
        }
        
        int highest_bit_so(unsigned long long x)
        {
          static const unsigned long long t[6] = {
            0xFFFFFFFF00000000ull,
            0x00000000FFFF0000ull,
            0x000000000000FF00ull,
            0x00000000000000F0ull,
            0x000000000000000Cull,
            0x0000000000000002ull
          };
        
          int y = (((x & (x - 1)) == 0) ? 0 : 1);
          int j = 32;
          int i;
        
          for (i = 0; i < 6; i++) {
            int k = (((x & t[i]) == 0) ? 0 : j);
            y += k;
            x >>= k;
            j >>= 1;
          }
        
          return y;
        }
        
        int highest_bit_so2(unsigned long long value)
        {
          int pos = 0;
          if (value & (value - 1ULL))
          {
            pos = 1;
          }
          if (value & 0xFFFFFFFF00000000ULL)
          {
            pos += 32;
            value = value >> 32;
          }
          if (value & 0x00000000FFFF0000ULL)
          {
            pos += 16;
            value = value >> 16;
          }
          if (value & 0x000000000000FF00ULL)
          {
            pos += 8;
            value = value >> 8;
          }
          if (value & 0x00000000000000F0ULL)
          {
            pos += 4;
            value = value >> 4;
          }
          if (value & 0x000000000000000CULL)
          {
            pos += 2;
            value = value >> 2;
          }
          if (value & 0x0000000000000002ULL)
          {
            pos += 1;
            value = value >> 1;
          }
          return pos;
        }
        

        这是highest-bit.h

        int highest_bit_unrolled(unsigned long long n);
        int highest_bit_bs(unsigned long long n);
        int highest_bit_shift(unsigned long long n);
        int highest_bit_parallel(unsigned long long n);
        int highest_bit_so(unsigned long long n);
        int highest_bit_so2(unsigned long long n);
        

        还有主程序(抱歉所有的复制粘贴):

        #include <stdlib.h>
        #include <stdio.h>
        #include <time.h>
        #include "highest-bit.h"
        
        double timedelta(clock_t start, clock_t end)
        {
          return (end - start)*1.0/CLOCKS_PER_SEC;
        }
        
        int main(int argc, char **argv)
        {
          int i;
          volatile unsigned long long v;
          clock_t start, end;
        
          start = clock();
        
          for (i = 0; i < 10000000; i++) {
            for (v = 0x8000000000000000; v; v >>= 1)
              highest_bit_unrolled(v);
          }
        
          end = clock();
        
          printf("highest_bit_unrolled = %6.3fs\n", timedelta(start, end));
        
          start = clock();
        
          for (i = 0; i < 10000000; i++) {
            for (v = 0x8000000000000000; v; v >>= 1)
              highest_bit_parallel(v);
          }
        
          end = clock();
        
          printf("highest_bit_parallel = %6.3fs\n", timedelta(start, end));
        
          start = clock();
        
          for (i = 0; i < 10000000; i++) {
            for (v = 0x8000000000000000; v; v >>= 1)
              highest_bit_bs(v);
          }
        
          end = clock();
        
          printf("highest_bit_bs = %6.3fs\n", timedelta(start, end));
        
          start = clock();
        
          for (i = 0; i < 10000000; i++) {
            for (v = 0x8000000000000000; v; v >>= 1)
              highest_bit_shift(v);
          }
        
          end = clock();
        
          printf("highest_bit_shift = %6.3fs\n", timedelta(start, end));
        
          start = clock();
        
          for (i = 0; i < 10000000; i++) {
            for (v = 0x8000000000000000; v; v >>= 1)
              highest_bit_so(v);
          }
        
          end = clock();
        
          printf("highest_bit_so = %6.3fs\n", timedelta(start, end));
        
          start = clock();
        
          for (i = 0; i < 10000000; i++) {
            for (v = 0x8000000000000000; v; v >>= 1)
              highest_bit_so2(v);
          }
        
          end = clock();
        
          printf("highest_bit_so2 = %6.3fs\n", timedelta(start, end));
        
          return 0;
        }
        

        我已经尝试过各种新旧 Intel x86 机器。

        highest_bit_unrolled(展开的二进制搜索)始终比highest_bit_parallel(无分支位操作)快得多。这比highest_bit_bs(二分搜索循环)快,反过来又比highest_bit_shift(简单的移位和计数循环)快。

        highest_bit_unrolled 也比接受的 SO 答案 (highest_bit_so) 和另一个答案 (highest_bit_so2) 中给出的更快。

        基准测试循环通过覆盖连续位的一位掩码。这是为了尝试在展开的二分搜索中击败分支预测,这是现实的:在现实世界的程序中,输入案例不太可能表现出位位置的局部性。

        这是旧的Intel(R) Core(TM)2 Duo CPU E4500 @ 2.20GHz 的结果:

        $ ./highest-bit
        highest_bit_unrolled =  6.090s
        highest_bit_parallel =  9.260s
        highest_bit_bs = 19.910s
        highest_bit_shift = 21.130s
        highest_bit_so =  8.230s
        highest_bit_so2 =  6.960s
        

        在较新的型号上Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz

        highest_bit_unrolled =  1.555s
        highest_bit_parallel =  3.420s
        highest_bit_bs =  6.486s
        highest_bit_shift =  9.505s
        highest_bit_so =  4.127s
        highest_bit_so2 =  1.645s
        

        在较新的硬件上,highest_bit_so2 在较新的硬件上更接近 highest_bit_unrolled。顺序不太一样;现在highest_bit_so 真的落后了,而且比highest_bit_parallel 慢。

        最快的highest_bit_unrolled 包含最多的代码和最多的分支。每个返回值都由一组不同的条件通过自己的专用代码达到。

        “避免所有分支”的直觉(由于担心分支错误预测)并不总是正确的。现代(甚至不再那么现代)处理器包含相当多的狡猾,以免受到分支的阻碍。


        附: highest_bit_unrolled 是在 December 2011 的 TXR 语言中引入的(有错误,因为已调试)。

        最近,我开始想知道一些没有分支的更好、更紧凑的代码是否可能不会更快。

        我对结果有些惊讶。

        可以说,对于 GNU C 并使用一些编译器原语,代码实际上应该是 #ifdef-ing,但就可移植性而言,该版本仍然存在。

        【讨论】:

        • 这是个好作品。我认为你在这个问题上进入了新的领域。承认这一点,可能值得重新评估球门柱的位置。在没有证据的情况下,我猜想这里的加速取决于基准中增量 for 循环的可预测性,虽然它有它的应用,但更现实的模型可能是随机抽样,它为产生合法时间增加了自己的障碍.您可能已经打破了这个问题,因此修复它意味着以有意义和有用的方式重新构建它,这可能会也可能不会。
        • 不错。我怀疑highest_bit_unrolled 最快的原因是它小心地不发出单个CPU 存储周期。只有获取周期,CPU 才能真正向前推进。我不是专家,但这似乎表明,总体而言,总线一致性延迟比分支错误预测惩罚(这符合我粗略的直觉)要昂贵得多。
        • @GlennSlayden 我们必须检查机器代码才能确定,但​​存储到局部变量应该只转换为寄存器操作。但是,那里存在不并行化的数据流依赖关系。例如,在highest_bit_so2 函数中,对输出累加器的每次加法都取决于检查输入值,该输入值被有条件地移位。所以这个寄存器不能轻易地映射到一组寄存器以并行化代码;并且会有管道停顿。
        【解决方案13】:

        如果您有 80 位或 128 位浮点数可用,请转换为该类型,然后读取指数位。此链接包含详细信息(最多 52 位的整数)和其他几种方法:

        http://graphics.stanford.edu/~seander/bithacks.html#IntegerLogIEEE64Float

        另外,检查 ffmpeg 源。我知道他们有一个非常快的算法。即使它不能直接扩展到更大的尺寸,你也可以轻松地做类似if (x&gt;INT32_MAX) return fastlog2(x&gt;&gt;32)+32; else return fastlog2(x);

        【讨论】:

          【解决方案14】:

          朴素线性搜索可能是均匀分布整数的一种选择,因为它平均需要略少于 2 次比较(对于任何整数大小)。

          /* between 1 and 64 comparisons, ~2 on average */
          #define u64_size(c) (              \
              0x8000000000000000 < (c) ? 64  \
            : 0x4000000000000000 < (c) ? 63  \
            : 0x2000000000000000 < (c) ? 62  \
            : 0x1000000000000000 < (c) ? 61  \
          ...
            : 0x0000000000000002 < (c) ?  2  \
            : 0x0000000000000001 < (c) ?  1  \
            :                             0  \
          )
          

          【讨论】:

          • 整数几乎从不均匀分布在 0..UINT64_MAX 上。尤其不是您要在其上使用此函数的整数。小数定律是计算机程序中的大多数数字在大多数情况下都很小,针对这种情况进行优化是一种胜利。不要投反对票,因为这是一个有趣的观察,并且对于它适用的极少数情况(并且目标机器没有硬件指令)很有用。
          • @PeterCordes 除了... 精心制定的哈希码的明确目标是尽可能统一地制定,只要此代码适合该条件(如声称的那样),它的实用性可能不会那么模糊。
          • @GlennSlayden:在哈希码上需要 ilog2 是否常见?对于在哈希表中使用,您需要对哈希表的大小取模,假设您将大小保持为 2 的幂,只需一个 AND。我确信有一些用例涉及均匀分布的全范围输入,但哈希似乎不太可能是一个例子,除非我错过了一个有意义的用例。
          • @PeterCordes 哦,我明白你的意思了。我暂时对需要在我的实际用例中执行log 操作感到困惑,而不是更普遍地给出小数定律的反例。作为记录,YouTube 发布的 videoId(64 位)或 channelId(128 位)值基本上是指示大小的(不透明、永久)整数,它们是在各个范围内的分布惊人地均匀。但是,是的,它们本质上是内容的代理令牌,所以虽然我的应用程序当然需要散列 它们(很多),但几乎不需要对它们执行 log
          【解决方案15】:

          一种对最高位的二进制搜索。

          在 stanford.edu 上发布的按位运算技巧的良好参考:Bit Twiddling Hacks。 该参考资料中有很多示例,包括用于计算 log2 值的几种算法。这包括与此类似的——也许更好的——方法。

          我的想法是,对最高有效位进行二进制搜索会比简单解决方案的最大 60 多个循环更快。该算法在每次迭代中将 n 移位其最后一次移位值的一半,在 msb 上为零。

          对于 64 位值,需要 6 次迭代才能确定最高有效位的位置 - 即n 的下限 log2。

          根据我的测量,这种方法的性能并不比下面第二个示例中的简单循环好多少。

          // floor log2 in 6 max iterations for unsigned 64-bit values.
          //
          int floor_log2(unsigned long long n)
          {
              int shval = 32;
              int msb   =  0;
              while (shval) {
                  if (n >> shval) {
                      msb += shval;
                      n  >>= shval;
                  }
                  shval >>= 1;
              }
              return msb;
          }
          
          int ceil_log2(unsigned long long n) 
          {
              return floor_log2(2 * n - 1);
          }
          

          简单算法:

          int floor_log2_simple(unsigned long long n)
          {
              int c =  0;
              while (n) {
                  n >>= 1;
                  c++;
              }
              return c - 1;
          }
          

          在 Rust 中实现的这些相同算法的性能几乎相同。顶级方法的性能稍微好一点——但平均只有几纳秒。再多的调整算法也不会带来更好的性能。

          嗯……

          【讨论】:

          • 这在最坏的情况下会进行 6 次迭代,而不是 4 次。
          • 你是对的 - 但它实际上每次都会进行 6 次迭代。
          猜你喜欢
          • 2016-02-21
          • 1970-01-01
          • 1970-01-01
          • 2011-08-31
          • 2013-08-30
          • 1970-01-01
          • 1970-01-01
          • 2016-08-19
          • 2020-12-08
          相关资源
          最近更新 更多