查找C中整数的最高设置位(msb)的最快/最有效方法是什么?


119

如果我有一些整数n,并且我想知道最高有效位的位置(也就是说,如果最低有效位在右边,那么我想知道最远的左位为1),找出最快/最有效的方法是什么?

我知道POSIX ffs()在strings.h中支持一种方法来查找第一个设置位,但是似乎没有相应的fls()方法。

是否有一些我确实不知道的真正明显的方法?

如果无法使用POSIX函数进行可移植性该怎么办?

编辑:在32位和64位体系结构上均可使用的解决方案怎么样(许多代码清单似乎只在32位int上运行)。


这里有一些实现:graphics.stanford.edu/~seander/bithacks.html#ZerosOnRightLinear(编辑:重新阅读您的问题后,我意识到上面的链接用于找到最右边的设置位,而不是您需要的最左边的位,尽管没有一种字的大小,这是一个棘手的问题)
支出者

2
请参阅《黑客的喜悦》中前导零算法的数量 ” 。
Darius Bacon

右边的数为零; 问题是左边的零。至少在快速浏览中我看不到它。
Darius Bacon

2
您是专门想要位数“ n”还是2 ^ n足够?
Alnitak

1
看一下“对数基数2”算法-正如安德森在文章中所说:“整数的对数基数2与最高位集(或最高有效位集,MSB)的位置相同”
Michael Burr

Answers:


64

GCC具有

 -内置函数:int __builtin_clz(unsigned int x)
     返回X中从0开始的前导0位的数量
     有效位位置。如果X为0,则结果不确定。

 -内置函数:int __builtin_clzl(unsigned long)
     类似于__builtin_clz,除了参数类型为unsigned
     长'。

 -内置函数:int __builtin_clzll(unsigned long long)
     类似于__builtin_clz,除了参数类型为unsigned
     很久很久。

我希望它们可以被转换为适合您当前平台的某种有效方式,无论是那些奇特的位旋转算法之一还是一条指令。


一个有用的技巧,如果你的输入可以是零是__builtin_clz(x | 1):无条件设置低位,而无需修改任何其他使输出31x=0,在不改变输出的任何其他输入。

为了避免这样做,您的另一个选择是特定于平台的内部函数,例如ARM GCC __clz(不需要标头)或_lzcnt_u32支持lzcnt指令的CPU上的x86 。(请注意,lzcntbsr在较旧的CPU上进行解码而不是出错,这会为非零输入提供31-lzcnt。)

不幸的是,没有办法在非x86平台上可移植地利用各种CLZ指令,这些指令确实将input = 0的结果定义为32或64(根据操作数宽度)。x86 lzcnt也会这样做,同时bsr会生成一个位索引,除非您使用,否则编译器必须翻转该位索引31-__builtin_clz(x)

(“未定义结果”不是C未定义行为,只是一个未定义的值。实际上是指令运行时目标寄存器中的值。AMD记录了这一点,英特尔没有,但是英特尔的CPU确实实现了该行为。 。但是,这并不是您之前要分配给C变量的内容,而是gcc将C转换为asm时通常无法正常工作。另请参见为什么破坏LZCNT的“输出依赖关系”很重要?


5
MSVC将具有_BitScanReverse
棘轮怪胎

1
零未定义行为使它们即使在LZCNT不可用时也可以编译为x86上的单个BSR指令。这对于__builtin_ctzover来说是一个很大的优势ffs,它将编译为BSF和CMOV以处理输入为零的情况。在没有足够短的实现的体系结构上(例如,没有clz指令的旧ARM ),gcc发出对libgcc helper函数的调用。
彼得·科德斯

41

假设您正在使用x86和游戏进行一些内联​​汇编程序,英特尔将提供一条BSR说明(“反向位扫描”)。在某些 x86 上速度很快(在其他x86上微编码)。从手册中:

在源操作数中搜索最高有效位(1位)。如果找到最高有效的1位,则其位索引存储在目标操作数中。源操作数可以是寄存器或内存位置;目标操作数是一个寄存器。位索引是源操作数的位0的无符号偏移量。如果内容源操作数为0,则目标操作数的内容未定义。

(如果您使用的是PowerPC,则有类似的cntlz(“前导零计数”)指令。)

gcc的示例代码:

#include <iostream>

int main (int,char**)
{
  int n=1;
  for (;;++n) {
    int msb;
    asm("bsrl %1,%0" : "=r"(msb) : "r"(n));
    std::cout << n << " : " << msb << std::endl;
  }
  return 0;
}

另请参见此内联汇编器教程,该教程显示(第9.4节)比循环代码快得多。


4
实际上,该指令通常被微编码到一个循环中,并且速度很慢。
rlbond

2
哪一个 ?BSR还是CNTLZ?当我阅读上面引用的x86-timing.pdf时,BSR仅在Netburst Pentium上很慢。我对PowerPC一无所知。
09年

5
...好的,在仔细检查后,请确保“ BSR仅在P3 / Pentium-M / Core2 x86上速度很快”。Netburst和AMD的速度慢。
09年

1
请注意:您的最后两个链接已失效。
Baum mit Augen

2
@rlbond:呵呵,P4 Prescott上的BSR为2 uops,具有16个循环延迟(!),每4c吞吐量中有一个。但是在较早的Netburst上,它只有4个周期的延迟(仍为2微秒),每2c吞吐率只有1个。(来源:agner.org/optimize)。在大多数CPU上,它还依赖于gcc不能解释的输出(当输入为零时,实际行为是保持目标不变)。这可能会导致诸如stackoverflow.com/questions/25078285/…之类的问题。IDK为什么gcc修复该错误时会错过BSR。
彼得·科德斯

38

由于2 ^ N是仅设置第N个位(1 << N)的整数,因此找到最高设置位的位置(N)是该整数的对数整数2。

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

unsigned int v;
unsigned r = 0;

while (v >>= 1) {
    r++;
}

这种“显而易见的”算法可能并非对所有人都透明,但是当您意识到代码反复向右移一位直到最左边的位被移走(请注意,C将任何非零值视为true)并返回数字时,的变化,这是很有意义的。这也意味着即使设置了多个位,它也可以工作-结果始终是最高有效位。

如果您向下滚动该页面,则会有更快,更复杂的版本。但是,如果您知道要处理带有很多前导零的数字,那么天真的方法可能会提供可接受的速度,因为C语言中的位移位相当快,而且简单的算法不需要索引数组。

注意:使用64位值时,请谨慎使用额外的智能算法;它们中的许多仅对于32位值正确工作。


2
@Johan逐步调试器可以帮助解释为什么退出循环。基本上是'',因为一旦最后一位向右移后,条件中的表达式的计算结果就为0(被视为false)。
奎因·泰勒

2
使用这样的最终结果的好主意:)
Johan

6
注意:必须为无符号,对于有符号整数,负数的右移将失败。
Xantix 2012年

2
Xantix:C / C ++中的转换是逻辑上的转换,因此可以正常工作。对于Java,JavaScript或D,您需要使用逻辑移位运算符>>>。加上比较器!= 0,以及一些未指定的括号。
Chase

8
@追逐:不,不是。对于unsigned来说,这是合乎逻辑的转变。对于signed,这可能是逻辑上的转变,也可能不是逻辑上的转变(事实上,通常是算术运算)。
TimČas2015年

17

这应该快如闪电:

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

25
7位移位,5位或指令,多个位和潜在的高速缓存未命中。:)您是否对它进行了基准测试,或查看生成的汇编程序?它可能最终会非常慢,这取决于编译器可以多少它消除。
jalf

5
我是新来的。我没有得到反对票。我提供了唯一可行的源代码答案。
主角

9
“可能的高速缓存未命中”可能是由于此代码需要访问其查找表。如果在调用该表时未缓存该表,则在获取该表时将出现停顿。这可能使最坏情况下的性能远比不使用LUT的解决方案差。
放松

13
不是很重要。它使用了比所需更多的数据缓存(甚至超过了一条缓存行),并且使用了比必要更多的指令缓存。您很可能会在第一次调用该函数时就避免了缓存未命中,并且它将对缓存造成的污染超过了必要,因此调用之后,其他代码可能会遇到不必要的未命中事件。LUT通常不值得麻烦,因为高速缓存未命中的代价很高。但是我只是说这是我要标榜的东西,然后才声称它“快如闪电”。并不是说这绝对是一个问题。
jalf

6
该表包含32个条目,每个值<255(127),因此将表定义为unsigned char类型,它将适合于一个32字节的L1高速缓存行。整个过程适合两个缓存行。
ChuckCottrill

16

这有点像查找整数日志。有一些花哨的技巧,但是我为此制作了自己的工具。当然,目标是提高速度。

我的认识是CPU已经有一个自动位检测器,用于整数到浮点转换!所以用那个。

double ff=(double)(v|1);
return ((*(1+(uint32_t *)&ff))>>20)-1023;  // assumes x86 endianness

此版本将值强制转换为双精度,然后读取指数,从而告诉您该位在何处。花式移位和减去是从IEEE值中提取适当的部分。

使用浮点数的速度稍快一些,但是浮点数只能提供前24位的位置,因为它的精度较低。


为了安全地执行此操作,而在C ++或C中没有未定义的行为,请使用memcpy而不是指针强制转换进行类型处理。编译器知道如何有效地内联它。

// static_assert(sizeof(double) == 2 * sizeof(uint32_t), "double isn't 8-byte IEEE binary64");
// and also static_assert something about FLT_ENDIAN?

double ff=(double)(v|1);

uint32_t tmp;
memcpy(&tmp, ((const char*)&ff)+sizeof(uint32_t), sizeof(uint32_t));
return (tmp>>20)-1023;

或者在C99及更高版本中,使用union {double d; uint32_t u[2];};。但是请注意,在C ++中,仅某些编译器将扩​​展作为联合类型修剪,而不是在ISO C ++中。


这通常比前导零计数指令的特定于平台的固有速度慢,但是可移植的ISO C没有这种功能。一些CPU也缺少前导零计数指令,但是其中一些可以将整数有效地转换为double。但是,将FP位模式类型化回整数可能会很慢(例如,在PowerPC上,它需要存储/重新加载,通常会导致加载命中-存储停顿)。

该算法可能对SIMD实现很有用,因为具有SIMD的CPU更少lzcnt。x86仅在AVX512CD上获得了这样的指令


2
是。由于类型别名优化,gcc会使用-O2这样的代码来处理令人讨厌的事情。
MSN

4
在x86 CPU上,整数和浮点之间的强制转换可能会非常昂贵
jalf

1
是的,FPU成本很高。但是实际时间测量表明,这比全位运算或任何循环都快。尝试一下并采取最快的方法永远是最好的建议。我对此没有GCC和-O2的问题。
SPWorley

1
这不是未定义的行为(通过不兼容类型的指针读取值)吗?
dreamlax 2011年

3
Hacker's Delight解释了如何在5-3计数前0中纠正32位浮点数中的错误。这是他们的代码,该代码使用匿名联合将asFloat和asInt重叠:k = k&〜(k >> 1); asFloat =(float)k + 0.5f; n = 158-(asInt >> 23); (是的,这取决于实现定义的行为)
D Coetzee

11

卡兹(Kaz Kylheku)在这里

我为此在63个以上的位数(gcc x86_64上的long long类型)上确定了两种方法的基准,并且远离符号位。

(您发现我碰巧需要此“查找最高位”)

我实现了数据驱动的二进制搜索(紧密基于以上答案之一)。我还手动实现了一个完全展开的决策树,它只是带有立即操作数的代码。没有循环,没有表。

基准树(highest_bit_unrolled)的基准测试速度提高了69%,但n = 0的情况除外(二进制搜索对其进行了显式测试)。

二进制搜索针对0情况的特殊测试仅比没有特殊测试的决策树快48%。

机器编译器:(GCC 4.5.2,-O3,x86-64、2867 Mhz Intel Core i5)。

int highest_bit_unrolled(long long n)
{
  if (n & 0x7FFFFFFF00000000) {
    if (n & 0x7FFF000000000000) {
      if (n & 0x7F00000000000000) {
        if (n & 0x7000000000000000) {
          if (n & 0x4000000000000000)
            return 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(long long n)
{
  const 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;
}

快速而肮脏的测试程序:

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

int highest_bit_unrolled(long long n);
int highest_bit(long long n);

main(int argc, char **argv)
{
  long long n = strtoull(argv[1], NULL, 0);
  int b1, b2;
  long i;
  clock_t start = clock(), mid, end;

  for (i = 0; i < 1000000000; i++)
    b1 = highest_bit_unrolled(n);

  mid = clock();

  for (i = 0; i < 1000000000; i++)
    b2 = highest_bit(n);

  end = clock();

  printf("highest bit of 0x%llx/%lld = %d, %d\n", n, n, b1, b2);

  printf("time1 = %d\n", (int) (mid - start));
  printf("time2 = %d\n", (int) (end - mid));
  return 0;
}

仅使用-O2,差异会更大。决策树快了将近四倍。

我还以朴素的位移代码为基准:

int highest_bit_shift(long long n)
{
  int i = 0;
  for (; n; n >>= 1, i++)
    ; /* empty */
  return i;
}

正如人们所期望的那样,这仅对于少量用户而言是快速的。在确定n == 1的最高位为1时,基准测试速度提高了80%以上。但是,在63位空间中随机选择的数字的一半设置了第63位!

在输入0x3FFFFFFFFFFFFFFFFF上,决策树版本比1上快很多,并且显示出比移位器快1120%(12.2倍)。

我还将针对GCC内建函数对决策树进行基准测试,并尝试混合输入而不是重复相同的输入。可能会有一些分支分支预测正在进行中,也许还有一些不切实际的缓存场景,这使得它在重复上人为地变得更快。


9
我并不是说这不好,但是您的测试程序仅在相同的数字上进行测试,在2-3次迭代之后,分支预测变量将设置为最终位置,然后它们将做出完美的分支预测。好消息是,在完全随机分布的情况下,一半的数字将接近完美的预测,即bit63。
Surt 2014年


6
unsigned int
msb32(register unsigned int x)
{
        x |= (x >> 1);
        x |= (x >> 2);
        x |= (x >> 4);
        x |= (x >> 8);
        x |= (x >> 16);
        return(x & ~(x >> 1));
}

1个寄存器,13条指令。信不信由你,这通常比上面提到的以线性时间运行的BSR指令要快。这是对数时间。

来自http://aggregate.org/MAGIC/#Most%20Significant%201%20Bit


7
上面的代码无法回答问题。它返回一个无符号整数,其中x中的最高有效位保持打开状态,而所有其他位都关闭。问题是返回位置最显著对位。
主角

3
然后,您可以使用De Bruijn序列方法来查找设置的位的索引。:-)
R .. GitHub停止帮助ICE,

5
@主角,他在评论中说,只要满足即可。
rlbond 2011年

该页面(在同一页面上)可以满足您的需求,但它需要附加功能。gregate.org/MAGIC/#Log2%20of%20an%20Integer
Quinn Taylor

1
至少从Core2开始,BSR在Intel CPU上速度很快。LZCNT在AMD CPU上运行速度很快,并且gcc将它用于__builtin_clz启用了-march=native或其他功能的情况(因为在支持它的每个CPU上运行速度都很快)。即使在像Bull慢的AMD Bulldozer系列这样的CPU上,也不是那么慢:7 m-ops具有4个周期的延迟和每4c吞吐量一个。在Atom上,BSR 确实很慢:16个周期。在Silvermont上,这是10微秒,有10个周期的延迟。这可能比Silvermont上的BSR(但IDK)略低。
彼得·科德斯

6

这是本页当前提供的一些(简单)基准测试算法...

该算法尚未在unsigned int的所有输入上进行过测试;因此,请先检查一下,然后再盲目使用;)

在我的机器上,clz(__builtin_clz)和asm效果最佳。asm似乎比clz还要快...但这可能是由于简单的基准测试所致...

//////// go.c ///////////////////////////////
// compile with:  gcc go.c -o go -lm
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

/***************** math ********************/

#define POS_OF_HIGHESTBITmath(a) /* 0th position is the Least-Signif-Bit */    \
  ((unsigned) log2(a))         /* thus: do not use if a <= 0 */  

#define NUM_OF_HIGHESTBITmath(a) ((a)               \
                  ? (1U << POS_OF_HIGHESTBITmath(a))    \
                  : 0)



/***************** clz ********************/

unsigned NUM_BITS_U = ((sizeof(unsigned) << 3) - 1);
#define POS_OF_HIGHESTBITclz(a) (NUM_BITS_U - __builtin_clz(a)) /* only works for a != 0 */

#define NUM_OF_HIGHESTBITclz(a) ((a)                    \
                 ? (1U << POS_OF_HIGHESTBITclz(a))  \
                 : 0)


/***************** i2f ********************/

double FF;
#define POS_OF_HIGHESTBITi2f(a) (FF = (double)(ui|1), ((*(1+(unsigned*)&FF))>>20)-1023)


#define NUM_OF_HIGHESTBITi2f(a) ((a)                    \
                 ? (1U << POS_OF_HIGHESTBITi2f(a))  \
                 : 0)




/***************** asm ********************/

unsigned OUT;
#define POS_OF_HIGHESTBITasm(a) (({asm("bsrl %1,%0" : "=r"(OUT) : "r"(a));}), OUT)

#define NUM_OF_HIGHESTBITasm(a) ((a)                    \
                 ? (1U << POS_OF_HIGHESTBITasm(a))  \
                 : 0)




/***************** bitshift1 ********************/

#define NUM_OF_HIGHESTBITbitshift1(a) (({   \
  OUT = a;                  \
  OUT |= (OUT >> 1);                \
  OUT |= (OUT >> 2);                \
  OUT |= (OUT >> 4);                \
  OUT |= (OUT >> 8);                \
  OUT |= (OUT >> 16);               \
      }), (OUT & ~(OUT >> 1)))          \



/***************** bitshift2 ********************/
int POS[32] = {0, 1, 28, 2, 29, 14, 24, 3,
             30, 22, 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, 21, 19,
             16, 7, 26, 12, 18, 6, 11, 5, 10, 9};

#define POS_OF_HIGHESTBITbitshift2(a) (({   \
  OUT = a;                  \
  OUT |= OUT >> 1;              \
  OUT |= OUT >> 2;              \
  OUT |= OUT >> 4;              \
  OUT |= OUT >> 8;              \
  OUT |= OUT >> 16;             \
  OUT = (OUT >> 1) + 1;             \
      }), POS[(OUT * 0x077CB531UL) >> 27])

#define NUM_OF_HIGHESTBITbitshift2(a) ((a)              \
                       ? (1U << POS_OF_HIGHESTBITbitshift2(a)) \
                       : 0)



#define LOOPS 100000000U

int main()
{
  time_t start, end;
  unsigned ui;
  unsigned n;

  /********* Checking the first few unsigned values (you'll need to check all if you want to use an algorithm here) **************/
  printf("math\n");
  for (ui = 0U; ui < 18; ++ui)
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITmath(ui));

  printf("\n\n");

  printf("clz\n");
  for (ui = 0U; ui < 18U; ++ui)
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITclz(ui));

  printf("\n\n");

  printf("i2f\n");
  for (ui = 0U; ui < 18U; ++ui)
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITi2f(ui));

  printf("\n\n");

  printf("asm\n");
  for (ui = 0U; ui < 18U; ++ui) {
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITasm(ui));
  }

  printf("\n\n");

  printf("bitshift1\n");
  for (ui = 0U; ui < 18U; ++ui) {
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITbitshift1(ui));
  }

  printf("\n\n");

  printf("bitshift2\n");
  for (ui = 0U; ui < 18U; ++ui) {
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITbitshift2(ui));
  }

  printf("\n\nPlease wait...\n\n");


  /************************* Simple clock() benchmark ******************/
  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITmath(ui);
  end = clock();
  printf("math:\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITclz(ui);
  end = clock();
  printf("clz:\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITi2f(ui);
  end = clock();
  printf("i2f:\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITasm(ui);
  end = clock();
  printf("asm:\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITbitshift1(ui);
  end = clock();
  printf("bitshift1:\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITbitshift2(ui);
  end = clock();
  printf("bitshift2\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  printf("\nThe lower, the better. Take note that a negative exponent is good! ;)\n");

  return EXIT_SUCCESS;
}

6

尽管我可能只在绝对需要最佳性能的情况下才使用此方法(例如,编写涉及位板的棋盘游戏AI),但最有效的解决方案是使用嵌入式ASM。有关说明的代码,请参见此博客文章的“优化”部分。

[...],将bsrl汇编指令计算最显著位的位置。因此,我们可以使用以下asm语句:

asm ("bsrl %1, %0" 
     : "=r" (position) 
     : "r" (number));

扩展一下:标准循环解决方案(向左移动并检查MSB)可能是最易读的。在所有涉及位纠缠的情况下,都无法超越ASM的速度,尽管没有必要使代码混乱,除非有必要。骇客是一种介于两者之间的解决方案-采取另一种方式。
Noldorin

我想说对数取值是一种可读性很强的解决方案(检查生成的asm,以查看编译器是否可以对其进行优化以使用此asm指令)
jalf

有时,内联ASM解决方案要慢一些,具体取决于CPU微代码的实现。
rlbond

5
@rlbound:尽管我可能会误会,但我很难相信。在任何现代CPU上,人们都会认为它会被翻译成一条指令
...。– Noldorin

3
@Noldorin有点晚了,但是..按照定义,它是一条指令,但是如果将其微码化为rlbond,则表明该条指令可以在内部解码为一大堆µops。在AMD的微体系结构和Intel Atom上往往是这种情况,但是在普通的Intel微体系结构上,这是一个单一的过程。
哈罗德

4

我需要一个例程来执行此操作,并且在搜索网络(并找到此页面)之前,我想出了自己的基于二进制搜索的解决方案。虽然我敢肯定有人做过!它以固定的时间运行,并且比发布的“显而易见的”解决方案要快,尽管我并没有提出任何出色的主张,只是出于兴趣而发布它。

int highest_bit(unsigned int a) {
  static const unsigned int maskv[] = { 0xffff, 0xff, 0xf, 0x3, 0x1 };
  const unsigned int *mask = maskv;
  int l, h;

  if (a == 0) return -1;

  l = 0;
  h = 32;

  do {
    int m = l + (h - l) / 2;

    if ((a >> m) != 0) l = m;
    else if ((a & (*mask << l)) != 0) h = m;

    mask++;
  } while (l < h - 1);

  return l;
}

4

多数民众赞成在某种形式的二进制搜索,它适用于所有类型的(无符号!)整数类型

#include <climits>
#define UINT (unsigned int)
#define UINT_BIT (CHAR_BIT*sizeof(UINT))

int msb(UINT x)
{
    if(0 == x)
        return -1;

    int c = 0;

    for(UINT i=UINT_BIT>>1; 0<i; i>>=1)
    if(static_cast<UINT>(x >> i))
    {
        x >>= i;
        c |= i;
    }

    return c;
}

完成:

#include <climits>
#define UINT unsigned int
#define UINT_BIT (CHAR_BIT*sizeof(UINT))

int lsb(UINT x)
{
    if(0 == x)
        return -1;

    int c = UINT_BIT-1;

    for(UINT i=UINT_BIT>>1; 0<i; i>>=1)
    if(static_cast<UINT>(x << i))
    {
        x <<= i;
        c ^= i;
    }

    return c;
}

4
请考虑不要将ALL_CAPS用于typedefs或除预处理程序宏以外的任何其他内容。这是一个被广泛接受的惯例。
underscore_d 2015年

4

这里有些过于复杂的答案。仅当输入已经是2的幂时,才应使用Debruin技术,否则会有更好的方法。对于2输入的幂,Debruin是绝对最快的,甚至比_BitScanReverse我测试过的任何处理器都要快。但是,在一般情况下,_BitScanReverse(或在编译器中调用的任何内在函数)是最快的(尽管在某些CPU上可以对其进行微编码)。

如果不是固有功能,那么这里是处理常规输入的最佳软件解决方案。

u8  inline log2 (u32 val)  {
    u8  k = 0;
    if (val > 0x0000FFFFu) { val >>= 16; k  = 16; }
    if (val > 0x000000FFu) { val >>= 8;  k |= 8;  }
    if (val > 0x0000000Fu) { val >>= 4;  k |= 4;  }
    if (val > 0x00000003u) { val >>= 2;  k |= 2;  }
    k |= (val & 2) >> 1;
    return k;
}

请注意,与大多数其他答案不同,此版本不需要在末尾进行Debruin查找。它计算到位。

但是,表可能是更可取的,如果您反复调用多次,则表的加速会掩盖缓存未命中的风险。

u8 kTableLog2[256] = {
0,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7
};

u8 log2_table(u32 val)  {
    u8  k = 0;
    if (val > 0x0000FFFFuL) { val >>= 16; k  = 16; }
    if (val > 0x000000FFuL) { val >>=  8; k |=  8; }
    k |= kTableLog2[val]; // precompute the Log2 of the low byte

    return k;
}

在这里给出的所有软件答案中,这应该产生最高的吞吐量,但是如果您仅偶尔调用它,则更喜欢无表式解决方案,例如我的第一个代码段。


1
一些答案是无分支的,但这可能会与条件分支一起编译。您是否仅重复使用相同的值或简单的模式或其他东西进行基准测试?分支错误预测是性能的杀手kill。 stackoverflow.com/questions/11227809/…–
彼得·科德斯

3

正如上面的答案所指出的,有很多方法可以确定最高有效位。但是,正如还指出的那样,这些方法可能对于32位或64位寄存器都是唯一的。该stanford.edu bithacks页面提供的解决方案,无论对于32位和64位计算的工作。只需少量工作,便可以将它们组合起来,以提供一种可靠的跨体系结构方法来获得MSB。我在64位和32位计算机上编译/工作的解决方案是:

#if defined(__LP64__) || defined(_LP64)
# define BUILD_64   1
#endif

#include <stdio.h>
#include <stdint.h>  /* for uint32_t */

/* CHAR_BIT  (or include limits.h) */
#ifndef CHAR_BIT
#define CHAR_BIT  8
#endif  /* CHAR_BIT */

/* 
 * Find the log base 2 of an integer with the MSB N set in O(N)
 * operations. (on 64bit & 32bit architectures)
 */
int
getmsb (uint32_t word)
{
    int r = 0;
    if (word < 1)
        return 0;
#ifdef BUILD_64
    union { uint32_t u[2]; double d; } t;  // temp
    t.u[__FLOAT_WORD_ORDER==LITTLE_ENDIAN] = 0x43300000;
    t.u[__FLOAT_WORD_ORDER!=LITTLE_ENDIAN] = word;
    t.d -= 4503599627370496.0;
    r = (t.u[__FLOAT_WORD_ORDER==LITTLE_ENDIAN] >> 20) - 0x3FF;
#else
    while (word >>= 1)
    {
        r++;
    }
#endif  /* BUILD_64 */
    return r;
}

不是真的吗?最初在#ifdef BUILD_64标志上方定义?在这种情况下,将不需要在条件内重新定义。
David C. Rankin

3

使用逐次逼近的C版本:

unsigned int getMsb(unsigned int n)
{
  unsigned int msb  = sizeof(n) * 4;
  unsigned int step = msb;
  while (step > 1)
 {
    step /=2;
    if (n>>msb)
     msb += step;
   else
     msb -= step;
 }
  if (n>>msb)
    msb++;
  return (msb - 1);
}

优点:运行时间是恒定的,而与提供的数目无关,因为循环数始终相同。(使用“ unsigned int”时有4个循环)


如果使用三元运算符(msb += (n>>msb) ? step : -step;)编写,则更多的编译器可能会生成无分支的asm,从而避免了每一步的分支预测错误(stackoverflow.com/questions/11227809/…)。
彼得·科德斯

3

我知道这个问题很老,但是我自己实现了msb()函数后,发现此处和其他网站上提出的大多数解决方案不一定是最有效的-至少出于我个人对效率的定义(另请参见下面的更新))。原因如下:

大多数解决方案(尤其是那些采用某种二进制搜索方案或从右到左进行线性扫描的朴素方法的解决方案)似乎忽略了以下事实:对于任意二进制数,没有很多以很长的序列开头的零。实际上,对于任何位宽,所有整数的一半以1开头,四分之一以01开头。看看我要去哪里?我的观点是,从最高有效位位置到最低有效位(从左到右)开始的线性扫描并不像乍看上去那样“线性”。

可以显示1,对于任何位宽,需要测试的平均位数最多为2。这将转换为O(1)相对于位数(!)的摊销时间复杂度。。

当然,最坏的情况仍然是O(n),比通过类似二进制搜索的方法得到的O(log(n))还要糟糕,但是由于最坏的情况很少,因此对于大多数应用程序而言,它们可以忽略不计(Update:不完全:可能很少,但是它们发生的可能性很高-请参阅下面的更新)。

这是我提出的“天真的”方法,至少在我的机器上,它比大多数其他方法都要好(针对32位整数的二进制搜索方案始终需要log 2(32)= 5步,而这种愚蠢的算法需要更少的log平均超过2)-抱歉,这是C ++而不是纯C:

template <typename T>
auto msb(T n) -> int
{
    static_assert(std::is_integral<T>::value && !std::is_signed<T>::value,
        "msb<T>(): T must be an unsigned integral type.");

    for (T i = std::numeric_limits<T>::digits - 1, mask = 1 << i; i >= 0; --i, mask >>= 1)
    {
        if ((n & mask) != 0)
            return i;
    }

    return 0;
}

更新:虽然我在这里写的内容对于任意整数都是完全正确的,但每种位组合都是同等可能的(我的速度测试只是测量了确定所有 32位整数的MSB所花的时间),真实整数通常会以不同的方式调用该函数:例如,在我的代码中,此函数用于确定对象大小是2的幂还是查找大于或等于2的下一个幂对象大小。我的猜测是,大多数使用MSB的应用程序所涉及的数字远小于整数可以表示的最大数字(对象大小很少利用 size_t中的所有位)。在这种情况下,我的解决方案实际上将比二进制搜索方法执行得更差-因此,即使我的解决方案循环遍历所有整数的速度更快,也应首选后者。
TL; DR:现实生活中的整数可能会偏向于这种简单算法的最坏情况,这最终会使它的性能更差-尽管事实上对真正任意整数均摊销了 O(1)

1该参数如下所示(草稿):令n为位数(位宽)。总共可以用n位表示2个n整数。有2 N - 1个整数开始与1(第一1是固定的,其余的N - 1位可以是任何东西)。那些整数仅需要循环的一个中断即可确定MSB。此外,有2个n-2的整数(从01开始),需要2次迭代,2 n-3的整数,从001开始,需要3次迭代,依此类推。

如果将所有可能的整数的所有必需的迭代求和并除以2 n(整数总数),我们将获得确定n位整数的MSB所需的平均迭代数:

(1 * 2 n-1 + 2 * 2 n-2 + 3 * 2 n-3 + ... + n)/ 2 n

这一系列的平均迭代实际上是收敛的,对于无穷大,n的极限为2

因此,对于任何数量的位,幼稚的从左到右算法实际上都具有O(1)摊销常数时间复杂度。


2
我认为对msb函数的输入倾向于均匀分布并不一定是一个公平的假设。实际上,这些输入往往是中断寄存器或位板或其他一些具有不均匀分布值的数据结构。对于公平的基准,我认为假设输出(而不是输入)将平均分配是比较安全的。
johnwbyrd

3

给了我们log2。这消除了log2您在此页面上看到的所有特殊调味料实现的需求。您可以log2像下面这样使用标准的实现:

const auto n = 13UL;
const auto Index = (unsigned long)log2(n);

printf("MSB is: %u\n", Index); // Prints 3 (zero offset)

还需要注意nof 0UL,因为:

返回-∞并引发FE_DIVBYZERO

我已经编写了一个带有任意设置IndexULONG_MAX此处的支票的示例:https : //ideone.com/u26vsi


ephemient的gcc的推论唯一的答案是:

const auto n = 13UL;
unsigned long Index;

_BitScanReverse(&Index, n);
printf("MSB is: %u\n", Index); // Prints 3 (zero offset)

有关_BitScanReverse状态的文档为Index

加载找到的第一个设置位(1)的位位置

在实践中我发现,如果n0UL那个Index被设定为0UL,只因为这将是一个n1UL。但在一个情况下的文档中保证唯一n0UL是,返回的是:

如果未找到设置位,则为0

因此,类似于log2上面的优选实施方式,Index在这种情况下,应检查返回设置为标记值。我再次ULONG_MAX在此处编写了使用此标志值的示例:http : //rextester.com/GCU61409


否,当输入为时才_BitScanReverse返回0 。就像x86的指令一样,该指令仅根据输入而非输出来设置ZF。有趣的是,MS会在没有发现任何东西的情况下将文档说成未置位;也与的x86 asm行为匹配。(AMD将其记录为在src = 0上保留了目标寄存器未修改的状态,但是Intel只是说了未定义的输出,即使它们的CPU确实执行了未经修改的行为。)这与x86不同,后者提供了未找到的信息。0BSRindex1bsrlzcnt32
彼得·科德斯

@PeterCordes _BitScanReverse使用基于零的索引,因此,如果n为1,则设置位的索引实际上为0。不幸的是,正如您所说的,如果n为0,则输出也为0 :(这意味着无法使用return来区分n是1还是0,这就是我要传达的信息。您认为有更好的说法吗?
Jonathan Mee

我认为您是在谈论它的设置方式Index。那不是返回值。如果输入为零,则返回一个布尔值为false的布尔值(这就是为什么Index通过引用传递而不是正常返回)。 godbolt.org/g/gQKJdE。我检查了一下:尽管有MS文档的措辞,_BitScanReverse但也没有保留Index不变n==0:您只要在寄存器中获取碰巧要使用的任何值即可。(在您的情况下,该寄存器可能与Index之后使用的寄存器相同,导致您看到0)。
彼得·科德斯

这个问题没有标记为c ++。

@technosaurus谢谢,我忘了自己。考虑到问题是C,log2自C99以来,我们实际上就遇到过。
乔纳森·米

2

考虑按位运算符。

第一次我误解了这个问题。您应该使用最左边的位(其余为零)生成一个整数。假设cmp设置为该值:

position = sizeof(int)*8
while(!(n & cmp)){ 
   n <<=1;
   position--;
}

转换为字符串是什么意思?ffs的定义取一个int并返回一个int。转换将在哪里?如果我们在一个单词中寻找位,那么转换将有什么目的?
dreamlax

我不知道该功能。
瓦希尔

8应该是CHAR_BIT。这极不可能是最快的方法,因为分支错误预测将在退出循环时发生,除非将其重复与相同的输入重复使用。同样,对于小输入(很多零),它必须循​​环很多。这就像您在单元测试中用作易于验证版本以与优化版本进行比较的后备方式一样。
彼得·科德斯

2

扩展Josh的基准测试...可以如下改善clz

/***************** clz2 ********************/

#define NUM_OF_HIGHESTBITclz2(a) ((a)                              \
                  ? (((1U) << (sizeof(unsigned)*8-1)) >> __builtin_clz(a)) \
                  : 0)

关于asm:请注意,有bsr和bsrl(这是“长”版本)。正常的可能会快一点。


1

请注意,您要执行的操作是计算整数的整数log2,

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

unsigned int
Log2(unsigned long x)
{
    unsigned long n = x;
    int bits = sizeof(x)*8;
    int step = 1; int k=0;
    for( step = 1; step < bits; ) {
        n |= (n >> step);
        step *= 2; ++k;
    }
    //printf("%ld %ld\n",x, (x - (n >> 1)) );
    return(x - (n >> 1));
}

请注意,您可以尝试一次搜索超过1位。

unsigned int
Log2_a(unsigned long x)
{
    unsigned long n = x;
    int bits = sizeof(x)*8;
    int step = 1;
    int step2 = 0;
    //observe that you can move 8 bits at a time, and there is a pattern...
    //if( x>1<<step2+8 ) { step2+=8;
        //if( x>1<<step2+8 ) { step2+=8;
            //if( x>1<<step2+8 ) { step2+=8;
            //}
        //}
    //}
    for( step2=0; x>1L<<step2+8; ) {
        step2+=8;
    }
    //printf("step2 %d\n",step2);
    for( step = 0; x>1L<<(step+step2); ) {
        step+=1;
        //printf("step %d\n",step+step2);
    }
    printf("log2(%ld) %d\n",x,step+step2);
    return(step+step2);
}

这种方法使用二进制搜索

unsigned int
Log2_b(unsigned long x)
{
    unsigned long n = x;
    unsigned int bits = sizeof(x)*8;
    unsigned int hbit = bits-1;
    unsigned int lbit = 0;
    unsigned long guess = bits/2;
    int found = 0;

    while ( hbit-lbit>1 ) {
        //printf("log2(%ld) %d<%d<%d\n",x,lbit,guess,hbit);
        //when value between guess..lbit
        if( (x<=(1L<<guess)) ) {
           //printf("%ld < 1<<%d %ld\n",x,guess,1L<<guess);
            hbit=guess;
            guess=(hbit+lbit)/2;
            //printf("log2(%ld) %d<%d<%d\n",x,lbit,guess,hbit);
        }
        //when value between hbit..guess
        //else
        if( (x>(1L<<guess)) ) {
            //printf("%ld > 1<<%d %ld\n",x,guess,1L<<guess);
            lbit=guess;
            guess=(hbit+lbit)/2;
            //printf("log2(%ld) %d<%d<%d\n",x,lbit,guess,hbit);
        }
    }
    if( (x>(1L<<guess)) ) ++guess;
    printf("log2(x%ld)=r%d\n",x,guess);
    return(guess);
}

另一种可能更易读的二进制搜索方法,

unsigned int
Log2_c(unsigned long x)
{
    unsigned long v = x;
    unsigned int bits = sizeof(x)*8;
    unsigned int step = bits;
    unsigned int res = 0;
    for( step = bits/2; step>0; )
    {
        //printf("log2(%ld) v %d >> step %d = %ld\n",x,v,step,v>>step);
        while ( v>>step ) {
            v>>=step;
            res+=step;
            //printf("log2(%ld) step %d res %d v>>step %ld\n",x,step,res,v);
        }
        step /= 2;
    }
    if( (x>(1L<<res)) ) ++res;
    printf("log2(x%ld)=r%ld\n",x,res);
    return(res);
}

而且因为您将要测试这些,

int main()
{
    unsigned long int x = 3;
    for( x=2; x<1000000000; x*=2 ) {
        //printf("x %ld, x+1 %ld, log2(x+1) %d\n",x,x+1,Log2(x+1));
        printf("x %ld, x+1 %ld, log2_a(x+1) %d\n",x,x+1,Log2_a(x+1));
        printf("x %ld, x+1 %ld, log2_b(x+1) %d\n",x,x+1,Log2_b(x+1));
        printf("x %ld, x+1 %ld, log2_c(x+1) %d\n",x,x+1,Log2_c(x+1));
    }
    return(0);
}

1

由于这是“另一种”方法,因此将其放入似乎与已经给出的其他方法有所不同。

-1如果返回x==0,否则返回floor( log2(x)) (最大结果31)

从32位减少到4位问题,然后使用表。也许优雅,但务实。

__builtin_clz由于可移植性问题,这就是我不想使用的方式。

为了使其更紧凑,可以改为使用循环来减少,每次增加4到r,最多7次迭代。或某种混合形式,例如(对于64位):循环减少到8,测试减少到4。

int log2floor( unsigned x ){
   static const signed char wtab[16] = {-1,0,1,1, 2,2,2,2, 3,3,3,3,3,3,3,3};
   int r = 0;
   unsigned xk = x >> 16;
   if( xk != 0 ){
       r = 16;
       x = xk;
   }
   // x is 0 .. 0xFFFF
   xk = x >> 8;
   if( xk != 0){
       r += 8;
       x = xk;
   }
   // x is 0 .. 0xFF
   xk = x >> 4;
   if( xk != 0){
       r += 4;
       x = xk;
   }
   // now x is 0..15; x=0 only if originally zero.
   return r + wtab[x];
}

1

哇,那是很多答案。我很抱歉回答一个老问题。

int result = 0;//could be a char or int8_t instead
if(value){//this assumes the value is 64bit
    if(0xFFFFFFFF00000000&value){  value>>=(1<<5); result|=(1<<5);  }//if it is 32bit then remove this line
    if(0x00000000FFFF0000&value){  value>>=(1<<4); result|=(1<<4);  }//and remove the 32msb
    if(0x000000000000FF00&value){  value>>=(1<<3); result|=(1<<3);  }
    if(0x00000000000000F0&value){  value>>=(1<<2); result|=(1<<2);  }
    if(0x000000000000000C&value){  value>>=(1<<1); result|=(1<<1);  }
    if(0x0000000000000002&value){  result|=(1<<0);  }
}else{
  result=-1;
}

这个答案与另一个答案非常相似。


最好写出移位量1<<k。口罩呢?(1 << (1<<k-1)-1<< (1<<k-1)?(most optimal?您比较最高级吗?)
灰胡子

@greybeard如果您查看此问题的编辑内容,您将在我添加“最佳”部分时看到。当我更改答案时,我忘记将其删除。此外,我不知道为什么你正在谈论面具?(什么口罩?我没有关注您)
哈里·斯文森

(位掩码)是用于选择/清除在&和中有选择地使用的位的值&~。)您可以用代替十六进制常量((type)1<<(1<<k))-1<<(1<<k)
灰胡子

哦,对了,我使用口罩,我完全忘记了。我确实在两个月前回答了这个问题-嗯,好吧,因为它是在编译时评估的,所以我说它等于十六进制值。但是,一个是神秘的,一个是十六进制的。
哈里·斯文森

0

代码:

    // x>=1;
    unsigned func(unsigned x) {
    double d = x ;
    int p= (*reinterpret_cast<long long*>(&d) >> 52) - 1023;
    printf( "The left-most non zero bit of %d is bit %d\n", x, p);
    }

或通过设置Y = 1来获取FPU指令FYL2X(Y * Log2 X)的整数部分


嗯。什么?这个功能如何?它以任何方式可移植吗?
underscore_d 2015年

窗口中的代码是可移植的。函数FYL2X()是fpu指令,但可以移植并可以在某些FPU /数学库中找到。
jemin 2015年

@underscore_d之所以有效,是因为浮点数已归一化...转换为双精度尾数位以消除前导零,此代码提取指数并对其进行调整以确定移位的位数。它当然不是独立于体系结构,但是可能会在您遇到的任何计算机上运行。
Jim Balter

这是此答案的替代版本,有关性能和可移植性的说明,请参见此处。(特别是类型转换的指针强制转换的不可移植性。)它使用地址数学仅重新加载的高32位,double如果它确实存储/重新加载而不是通过其他方式进行类型双打,则可能很好。带有movq类似您可能会在x86上找到的说明。
彼得·科德斯

还要注意我的[对该答案的评论],在其中我提供了可怕的警告,即该方法对(至少)范围内的值给出了错误的答案[7FFFFFFFFFFFFE00 - 7FFFFFFFFFFFFFFF]
Glenn Slayden

0

另一位发帖人提供了一个使用字节宽查找的查找表。如果您想获得更高的性能(以32K的内存而不是256个查找条目为代价),这是一种使用15位查找表的解决方案,在C#7 for .NET中

有趣的部分是初始化表。由于这是我们在过程的整个生命周期中都需要的相对较小的块,因此我使用分配了非托管内存Marshal.AllocHGlobal。如您所见,为了获得最佳性能,整个示例均以本机形式编写:

readonly static byte[] msb_tab_15;

// Initialize a table of 32768 bytes with the bit position (counting from LSB=0)
// of the highest 'set' (non-zero) bit of its corresponding 16-bit index value.
// The table is compressed by half, so use (value >> 1) for indexing.
static MyStaticInit()
{
    var p = new byte[0x8000];

    for (byte n = 0; n < 16; n++)
        for (int c = (1 << n) >> 1, i = 0; i < c; i++)
            p[c + i] = n;

    msb_tab_15 = p;
}

该表需要通过上面的代码进行一次初始化。它是只读的,因此可以共享单个全局副本以进行并发访问。使用此表,您可以快速查找整数log 2,这是我们在此处查找的所有各种整数宽度(8、16、32和64位)。

请注意,表的表项0(未定义“最高设置位”概念的唯一整数)的值为-1。这种区别对于正确处理以下代码中的0值高位单词是必要的。事不宜迟,以下是各种整数基元各自的代码:

ulong(64位)版本

/// <summary> Index of the highest set bit in 'v', or -1 for value '0' </summary>
public static int HighestOne(this ulong v)
{
    if ((long)v <= 0)
        return (int)((v >> 57) & 0x40) - 1;      // handles cases v==0 and MSB==63

    int j = /**/ (int)((0xFFFFFFFFU - v /****/) >> 58) & 0x20;
    j |= /*****/ (int)((0x0000FFFFU - (v >> j)) >> 59) & 0x10;
    return j + msb_tab_15[v >> (j + 1)];
}

uint(32位)版本

/// <summary> Index of the highest set bit in 'v', or -1 for value '0' </summary>
public static int HighestOne(uint v)
{
    if ((int)v <= 0)
        return (int)((v >> 26) & 0x20) - 1;     // handles cases v==0 and MSB==31

    int j = (int)((0x0000FFFFU - v) >> 27) & 0x10;
    return j + msb_tab_15[v >> (j + 1)];
}

上面的各种重载

public static int HighestOne(long v) => HighestOne((ulong)v);
public static int HighestOne(int v) => HighestOne((uint)v);
public static int HighestOne(ushort v) => msb_tab_15[v >> 1];
public static int HighestOne(short v) => msb_tab_15[(ushort)v >> 1];
public static int HighestOne(char ch) => msb_tab_15[ch >> 1];
public static int HighestOne(sbyte v) => msb_tab_15[(byte)v >> 1];
public static int HighestOne(byte v) => msb_tab_15[v >> 1];

这是一个完整,有效的解决方案,代表了.NET 4.7.2上最佳性能,而我将其与专用性能测试工具进行了比较。其中一些在下面提到。测试参数是所有65位位置的均匀密度,即0 ... 31/63加值0(产生结果-1)。位低于所述目标索引位置,随机填充。测试仅是x64发行版模式,并且启用了JIT优化。




我的正式回答到此结束。以下是一些随意的注释,以及指向与我为验证上述代码的性能和正确性而进行的测试相关的替代测试候选人的源代码链接。


上面提供的版本编码为Tab16A,在许多运行中一直是赢家。您可以在这里这里这里找到活跃的工作/临时形式的各种候选人。

 1个候选人。HighestOne_Tab16A622,496
 2个候选人。HighestOne_Tab16C628,234
 3名候选人。HighestOne_Tab8A649,146
 4个候选人。HighestOne_Tab8B656,847
 5个候选人。HighestOne_Tab16B657,147
 6名候选人。HighestOne_Tab16D659,650
 7_highest_one_bit_UNMANAGED.HighestOne_U 702,900
 8 de_Bruijn.IndexOfMSB 709,672
 9 _old_2.HighestOne_Old2 715,810
10 _test_A.HighestOne8 757,188
11 _old_1.HighestOne_Old1 757,925
12 _test_A.HighestOne5(不安全)760,387
13 _test_B.HighestOne8(不安全)763,904
14 _test_A.HighestOne3(不安全)766433
15 _test_A.HighestOne1(不安全)767,321
16 _test_A.HighestOne4(不安全)771702
17 _test_B.HighestOne2(不安全)772,136
18 _test_B.HighestOne1(不安全)772,527
19 _test_B.HighestOne3(不安全)774,140
20 _test_A.HighestOne7(不安全)774,581
21 _test_B.HighestOne7(不安全)775,463
22 _test_A.HighestOne2(不安全)776,865
23位候选人。HighestOne_NoTab777,698
24 _test_B.HighestOne6(不安全)779,481
25 _test_A.HighestOne6(不安全)781,553
26 _test_B.HighestOne4(不安全)785,504
27 _test_B.HighestOne5(不安全)789,797
28 _test_A.HighestOne0(不安全)809,566
29 _test_B.HighestOne0(不安全)814,990
30 _highest_one_bit.HighestOne 824,345
30 _bitarray_ext.RtlFindMostSignificantBit 894,069
31位候选人。HighestOne_Naive898,865

值得注意的是ntdll.dll!RtlFindMostSignificantBitvia P / Invoke 的糟糕表现 :

[DllImport("ntdll.dll"), SuppressUnmanagedCodeSecurity, SecuritySafeCritical]
public static extern int RtlFindMostSignificantBit(ulong ul);

真的太糟糕了,因为这是整个实际功能:

    RtlFindMostSignificantBit:
        bsr rdx, rcx  
        mov eax,0FFFFFFFFh  
        movzx ecx, dl  
        cmovne      eax,ecx  
        ret

我无法想象这五行表现不佳,因此必须归咎于托管/本地过渡处罚。我也感到惊讶的是,该测试确实比32 short字节(和64KB)(16位)直接查找表胜过128字节(和256字节)byte(8位)查找表。我认为以下内容将与16位查找相比更具竞争力,但后者始终优于此查找:

public static int HighestOne_Tab8A(ulong v)
{
    if ((long)v <= 0)
        return (int)((v >> 57) & 64) - 1;

    int j;
    j =  /**/ (int)((0xFFFFFFFFU - v) >> 58) & 32;
    j += /**/ (int)((0x0000FFFFU - (v >> j)) >> 59) & 16;
    j += /**/ (int)((0x000000FFU - (v >> j)) >> 60) & 8;
    return j + msb_tab_8[v >> j];
}

我要指出的最后一件事是,我的deBruijn方法没有取得更好的成绩,我感到非常震惊。这是我以前普遍使用的方法:

const ulong N_bsf64 = 0x07EDD5E59A4E28C2,
            N_bsr64 = 0x03F79D71B4CB0A89;

readonly public static sbyte[]
bsf64 =
{
    63,  0, 58,  1, 59, 47, 53,  2, 60, 39, 48, 27, 54, 33, 42,  3,
    61, 51, 37, 40, 49, 18, 28, 20, 55, 30, 34, 11, 43, 14, 22,  4,
    62, 57, 46, 52, 38, 26, 32, 41, 50, 36, 17, 19, 29, 10, 13, 21,
    56, 45, 25, 31, 35, 16,  9, 12, 44, 24, 15,  8, 23,  7,  6,  5,
},
bsr64 =
{
     0, 47,  1, 56, 48, 27,  2, 60, 57, 49, 41, 37, 28, 16,  3, 61,
    54, 58, 35, 52, 50, 42, 21, 44, 38, 32, 29, 23, 17, 11,  4, 62,
    46, 55, 26, 59, 40, 36, 15, 53, 34, 51, 20, 43, 31, 22, 10, 45,
    25, 39, 14, 33, 19, 30,  9, 24, 13, 18,  8, 12,  7,  6,  5, 63,
};

public static int IndexOfLSB(ulong v) =>
    v != 0 ? bsf64[((v & (ulong)-(long)v) * N_bsf64) >> 58] : -1;

public static int IndexOfMSB(ulong v)
{
    if ((long)v <= 0)
        return (int)((v >> 57) & 64) - 1;

    v |= v >> 1; v |= v >> 2;  v |= v >> 4;   // does anybody know a better
    v |= v >> 8; v |= v >> 16; v |= v >> 32;  // way than these 12 ops?
    return bsr64[(v * N_bsr64) >> 58];
}

关于这个SO问题,有很多关于deBruijn方法的优缺点的讨论,我倾向于表示同意。我的猜测是,尽管deBruijn和直接查找表方法(我发现是最快的)都必须进行表查找,并且都具有非常小的分支,但是只有deBruijn才具有64位乘法运算。我只在IndexOfMSB这里测试了这些功能,而不是deBruijn,IndexOfLSB但是我希望后者的机会要多得多,因为它的操作少得多(请参见上文),并且我可能会继续将其用于LSB。


1
现代x86 CPU上的L1D缓存仅为32kiB。除非您反复使用相同的值,否则较大的LUT可能比较小的LUT更糟。如果不是这样,则会频繁发生缓存未命中。
彼得·科德斯

0

我谦虚的方法很简单:

MSB(x)= INT [Log(x)/ Log(2)]

转换:x的最高有效位是(基数x的对数除以基数2的对数)的整数值。

这可以轻松,快速地适应任何编程语言。在计算器上尝试一下,看看它是否有效。


如果您只想提高开发人员的效率,那将起作用。如果要提高运行时效率,则需要替代算法。
Mikko Rantalainen

由于舍入错误,这可能会失败。例如,在CPython 2和3中,int(math.log((1 << 48) - 1) / math.log(2))值为
48。– benrg

0

这是在GCCClang中工作的C的快速解决方案; 准备复制和粘贴。

#include <limits.h>

unsigned int fls(const unsigned int value)
{
    return (unsigned int)1 << ((sizeof(unsigned int) * CHAR_BIT) - __builtin_clz(value) - 1);
}

unsigned long flsl(const unsigned long value)
{
    return (unsigned long)1 << ((sizeof(unsigned long) * CHAR_BIT) - __builtin_clzl(value) - 1);
}

unsigned long long flsll(const unsigned long long value)
{
    return (unsigned long long)1 << ((sizeof(unsigned long long) * CHAR_BIT) - __builtin_clzll(value) - 1);
}

还有一个针对C ++的改进版本。

#include <climits>

constexpr unsigned int fls(const unsigned int value)
{
    return (unsigned int)1 << ((sizeof(unsigned int) * CHAR_BIT) - __builtin_clz(value) - 1);
}

constexpr unsigned long fls(const unsigned long value)
{
    return (unsigned long)1 << ((sizeof(unsigned long) * CHAR_BIT) - __builtin_clzl(value) - 1);
}

constexpr unsigned long long fls(const unsigned long long value)
{
    return (unsigned long long)1 << ((sizeof(unsigned long long) * CHAR_BIT) - __builtin_clzll(value) - 1);
}

该代码假定value不会0。如果要允许0,则需要对其进行修改。


0

我假设您的问题是整数(以下称为v),而不是无符号整数。

int v = 612635685; // whatever value you wish

unsigned int get_msb(int v)
{
    int r = 31;                         // maximum number of iteration until integer has been totally left shifted out, considering that first bit is index 0. Also we could use (sizeof(int)) << 3 - 1 instead of 31 to make it work on any platform.

    while (!(v & 0x80000000) && r--) {   // mask of the highest bit
        v <<= 1;                        // multiply integer by 2.
    }
    return r;                           // will even return -1 if no bit was set, allowing error catch
}

如果您想在不考虑符号的情况下使其正常工作,则可以添加一个额外的'v << = 1;'。在循环之前(并将r值相应地更改为30)。如果我忘了任何事,请告诉我。我没有测试过,但应该可以正常工作。


v <<= 1是时的未定义行为(UB)v < 0
chux-恢复莫妮卡

0x8000000,也许你的意思是那里多了0。
MM
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.