在C中找到整数中最高设置位(msb)的最快/最有效方法是啥?
Posted
技术标签:
【中文标题】在C中找到整数中最高设置位(msb)的最快/最有效方法是啥?【英文标题】:What is the fastest/most efficient way to find the highest set bit (msb) in an integer in C?在C中找到整数中最高设置位(msb)的最快/最有效方法是什么? 【发布时间】:2022-01-17 12:25:47 【问题描述】:如果我有一个整数 n,我想知道最高有效位的位置(也就是说,如果最低有效位在右边,我想知道最左边的位的位置是1)、最快/最有效的找出方法是什么?
我知道 POSIX 支持在 strings.h 中使用 ffs()
方法来查找第一个设置位,但似乎没有对应的 fls()
方法。
是否有一些我想念的非常明显的方法?
如果您不能使用 POSIX 函数来实现可移植性,该怎么办?
编辑:一个同时适用于 32 位和 64 位架构的解决方案怎么样(许多代码清单似乎只适用于 32 位整数)。
【问题讨论】:
这里有一些实现:graphics.stanford.edu/~seander/bithacks.html#ZerosOnRightLinear(编辑:重新阅读您的问题后,我意识到上面的链接是为了找到最右边的设置位,而不是您需要的最左边,尽管没有感觉字长,很难回答) 见Hacker's Delight中的“Number of leading zeros algorithms”。 在右边算零;问题是关于左边的零。至少,在快速浏览中我看不到它。 您是特别想要位数“n”,还是 2 ^ n 就足够了? 看看“Log Base 2”算法——正如 Anderson 在文章中所说:“整数的 log base 2 与最高位集(或最高有效位集)的位置相同, MSB)" 【参考方案1】:有一个建议在 C 中添加位操作函数,特别是前导零有助于找到最高位集。见http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2827.htm#design-bit-leading.trailing.zeroes.ones
希望它们尽可能作为内置实现,因此确保这是一种有效的方式。
这类似于最近添加到 C++ 中的内容(std::countl_zero
等)。
【讨论】:
【参考方案2】:这看起来很大,但与 bluegsmith 的循环相比,它的运行速度非常快
int Bit_Find_MSB_Fast(int x2)
long x = x2 & 0x0FFFFFFFFl;
long num_even = x & 0xAAAAAAAA;
long num_odds = x & 0x55555555;
if (x == 0) return(0);
if (num_even > num_odds)
if ((num_even & 0xFFFF0000) != 0) // top 4
if ((num_even & 0xFF000000) != 0)
if ((num_even & 0xF0000000) != 0)
if ((num_even & 0x80000000) != 0) return(32);
else
return(30);
else
if ((num_even & 0x08000000) != 0) return(28);
else
return(26);
else
if ((num_even & 0x00F00000) != 0)
if ((num_even & 0x00800000) != 0) return(24);
else
return(22);
else
if ((num_even & 0x00080000) != 0) return(20);
else
return(18);
else
if ((num_even & 0x0000FF00) != 0)
if ((num_even & 0x0000F000) != 0)
if ((num_even & 0x00008000) != 0) return(16);
else
return(14);
else
if ((num_even & 0x00000800) != 0) return(12);
else
return(10);
else
if ((num_even & 0x000000F0) != 0)
if ((num_even & 0x00000080) != 0)return(8);
else
return(6);
else
if ((num_even & 0x00000008) != 0) return(4);
else
return(2);
else
if ((num_odds & 0xFFFF0000) != 0) // top 4
if ((num_odds & 0xFF000000) != 0)
if ((num_odds & 0xF0000000) != 0)
if ((num_odds & 0x40000000) != 0) return(31);
else
return(29);
else
if ((num_odds & 0x04000000) != 0) return(27);
else
return(25);
else
if ((num_odds & 0x00F00000) != 0)
if ((num_odds & 0x00400000) != 0) return(23);
else
return(21);
else
if ((num_odds & 0x00040000) != 0) return(19);
else
return(17);
else
if ((num_odds & 0x0000FF00) != 0)
if ((num_odds & 0x0000F000) != 0)
if ((num_odds & 0x00004000) != 0) return(15);
else
return(13);
else
if ((num_odds & 0x00000400) != 0) return(11);
else
return(9);
else
if ((num_odds & 0x000000F0) != 0)
if ((num_odds & 0x00000040) != 0)return(7);
else
return(5);
else
if ((num_odds & 0x00000004) != 0) return(3);
else
return(1);
【讨论】:
【参考方案3】:使用 VPTEST(D, W, B) 和 PSRLDQ 指令的组合来关注包含最高有效位的字节,如下所示,在 Perl 中使用这些指令的仿真可在以下位置找到:
https://github.com/philiprbrenan/SimdAvx512
if (1) #TpositionOfMostSignificantBitIn64
my @m = ( # Test strings
#B0 1 2 3 4 5 6 7
#b0123456701234567012345670123456701234567012345670123456701234567
'0000000000000000000000000000000000000000000000000000000000000000',
'0000000000000000000000000000000000000000000000000000000000000001',
'0000000000000000000000000000000000000000000000000000000000000010',
'0000000000000000000000000000000000000000000000000000000000000111',
'0000000000000000000000000000000000000000000000000000001010010000',
'0000000000000000000000000000000000001000000001100100001010010000',
'0000000000000000000001001000010000000000000001100100001010010000',
'0000000000000000100000000000000100000000000001100100001010010000',
'1000000000000000100000000000000100000000000001100100001010010000',
);
my @n = (0, 1, 2, 3, 10, 28, 43, 48, 64); # Expected positions of msb
sub positionOfMostSignificantBitIn64($) # Find the position of the most significant bit in a string of 64 bits starting from 1 for the least significant bit or return 0 if the input field is all zeros
my ($s64) = @_; # String of 64 bits
my $N = 128; # 128 bit operations
my $f = 0; # Position of first bit set
my $x = '0'x$N; # Double Quad Word set to 0
my $s = substr $x.$s64, -$N; # 128 bit area needed
substr(VPTESTMD($s, $s), -2, 1) eq '1' ? ($s = PSRLDQ $s, 4) : ($f += 32); # Test 2 dwords
substr(VPTESTMW($s, $s), -2, 1) eq '1' ? ($s = PSRLDQ $s, 2) : ($f += 16); # Test 2 words
substr(VPTESTMB($s, $s), -2, 1) eq '1' ? ($s = PSRLDQ $s, 1) : ($f += 8); # Test 2 bytes
$s = substr($s, -8); # Last byte remaining
$s < $_ ? ++$f : last for # Search remaing byte
(qw(10000000 01000000 00100000 00010000
00001000 00000100 00000010 00000001));
64 - $f # Position of first bit set
ok $n[$_] eq positionOfMostSignificantBitIn64 $m[$_] for keys @m # Test
【讨论】:
这个问题是关于 C 而不是 Perl。 谢谢!对我没有帮助,但看起来确实很有趣:) 如果可以直接使用x86指令,使用一条bsr
或63-lzcnt
,而不是多条AVX-512指令!!这太疯狂了:一种非常复杂的提取比特集的方法,你可以用标量掩码完成,而不是向量字节移位和向量和 -> 掩码寄存器。【参考方案4】:
我假设您的问题是针对整数(下面称为 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
【讨论】:
v <<= 1
在v < 0
时是未定义的行为 (UB)。
0x8000000
,也许你的意思是一个额外的 0 。
请注意,测试 int32_t 变量的第 31 位是否为 1 可以简单地使用 v < 0
。不需要“复杂的”v & 0x80000000
。【参考方案5】:
GCC has:
-- 内置函数:int __builtin_clz (unsigned int x) 返回 X 中前导 0 位的数量,从最多开始 重要位的位置。如果 X 为 0,则结果未定义。 -- 内置函数:int __builtin_clzl (unsigned long) 类似于`__builtin_clz',除了参数类型是`unsigned 长'。 -- 内置函数:int __builtin_clzll (unsigned long long) 类似于`__builtin_clz',除了参数类型是`unsigned 长长的”。
我希望它们能够被翻译成对您当前平台相当有效的东西,无论是那些花哨的位旋转算法之一,还是单个指令。
如果你的输入可以为零,一个有用的技巧是__builtin_clz(x | 1)
:无条件地设置低位而不修改任何其他位使输出31
为x=0
,而不更改任何输出其他输入。
为避免需要这样做,您的另一个选择是特定于平台的内在函数,例如 ARM GCC 的 __clz
(不需要标头),或支持 lzcnt
指令的 CPU 上的 x86 的 _lzcnt_u32
。 (注意 lzcnt
在旧 CPU 上解码为 bsr
而不是故障,这会为非零输入提供 31-lzcnt。)
不幸的是,没有办法在非 x86 平台上可移植地利用各种 CLZ 指令,这些指令将 input=0 的结果定义为 32 或 64(根据操作数宽度)。 x86 的 lzcnt
也可以这样做,而 bsr
会生成一个位索引,除非您使用 31-__builtin_clz(x)
,否则编译器必须翻转它。
(“未定义的结果”不是 C 未定义的行为,只是一个未定义的值。它实际上是指令运行时目标寄存器中的任何内容。AMD 记录了这一点,英特尔没有,但英特尔的 CPU 有实现该行为。但它不是您分配给的 C 变量中的任何内容,当 gcc 将 C 转换为 asm 时,这通常不是事情的工作方式。另见 Why does breaking the "output dependency" of LZCNT matter?)
【讨论】:
MSVC 将拥有_BitScanReverse undefined-on-zero 行为使它们可以在 x86 上编译为单个 BSR 指令,即使 LZCNT 不可用。这是__builtin_ctz
相对于ffs
的一大优势,ffs
编译为 BSF 和 CMOV 来处理输入为零的情况。在没有足够短的实现的架构上(例如,没有clz
指令的旧 ARM),gcc 发出对 libgcc 辅助函数的调用。【参考方案6】:
这是一个适用于 GCC 和 Clang 的 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,则需要修改。
【讨论】:
【参考方案7】:我卑微的方法很简单:
MSB(x) = INT[Log(x) / Log(2)]
翻译:x的MSB是(Base x的Log除以Base 2的Log)的整数值。
这可以轻松快速地适应任何编程语言。在你的计算器上试一试,看看它是否有效。
【讨论】:
如果您只对开发人员的效率感兴趣,那就行得通。如果你想要运行时效率,你需要替代算法。 这可能由于舍入错误而失败。例如,在 CPython 2 和 3 中,int(math.log((1 << 48) - 1) / math.log(2))
是 48。【参考方案8】:
另一张海报使用byte-wide 查找提供了一个lookup-table。如果您想获得更多性能(以 32K 内存而不是仅 256 个查找条目为代价),这里有一个使用 15 位查找表的解决方案,在 C# 7 用于 .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;
表格需要通过上面的代码一次性初始化。它是只读的,因此可以共享单个全局副本以进行并发访问。使用此表,您可以快速查找整数 log2,这就是我们在此处查找的所有整数宽度(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 的版本在多次运行中始终是赢家。可以在here、here 和here 找到这些不同的候选人,以积极的工作/临时形式。
1名候选人。HighestOne_Tab16A 622,496 2名候选人。HighestOne_Tab16C 628,234 3 名候选人。HighestOne_Tab8A 649,146 4 名候选人。HighestOne_Tab8B 656,847 5 名候选人。HighestOne_Tab16B 657,147 6 名候选人。HighestOne_Tab16D 659,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(不安全)766,433 15 _test_A.HighestOne1(不安全)767,321 16 _test_A.HighestOne4(不安全)771,702 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_NoTab 777,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_Naive 898,865
值得注意的是ntdll.dll!RtlFindMostSignificantBit
通过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
我无法想象这五行导致的糟糕性能,因此必须归咎于托管/本地转换惩罚。令我感到惊讶的是,与 128 字节(和 256 字节)byte
(8 位)查找表相比,测试确实偏爱 32KB(和 64KB)short
(16 位)直接查找表。我认为以下内容与 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];
关于 deBruijn 方法 at this SO question 的优越性和出色性有很多讨论,我倾向于同意。我的猜测是,虽然 deBruijn 和直接查找表方法(我发现最快)都必须进行表查找,并且都具有非常小的分支,但只有 deBruijn 具有 64 位乘法运算。我在这里只测试了IndexOfMSB
函数——而不是deBruijn IndexOfLSB
——但我希望后者的机会更大,因为它的操作要少得多(见上文),我可能会继续使用它对于 LSB。
【讨论】:
现代 x86 CPU 上的 L1D 缓存只有 32kiB。除非您重复使用相同的值,否则大 LUT 可能比小 LUT 更差。否则,您将经常发生缓存未命中。 在对大型 LUT 进行基准测试时,您应该 1. 从数组中读取输入,以及 2. 先随机排列数组。这模拟了真实的应用程序行为:几乎没有人会使用来自循环归纳变量的参数调用此函数。它将来自某个地方的内存,因此会有一些缓存压力。当你这样做时,大 LUT 是一个可靠的失败者。甚至提出建议都是危险的,因为不了解的人可能会产生错误的想法。 deBruijn 方法如图所示很慢,因为它是一个大的串行数据依赖项,没有任何东西可以并行化。此类算法仅在有序标量 CPU 上节省时间。尝试打破依赖关系:ulong v1 = v>>1, v2 = v>>2, v3 = v>>3, vA = (v>>4)|v1|v2|v3, vA4 = vA>>4, vA8 = vA>>8, vA16 = vA>>16, vB = (vA>>24)|vA|vA4|vA8|vA16, v = vB|(vB>>32);
。随意检查这是否更快。至少在现代 Intel Core 上它不应该变慢,我预计它会花费大约 5/6 的时间。【参考方案9】:
c99 给了我们log2
。这消除了您在此页面上看到的所有特殊酱 log2
实现的需要。您可以像这样使用标准的log2
实现:
const auto n = 13UL;
const auto Index = (unsigned long)log2(n);
printf("MSB is: %u\n", Index); // Prints 3 (zero offset)
0UL
中的 n
也需要防范,因为:
-∞ 返回并引发 FE_DIVBYZERO
我已经编写了一个示例,该检查将Index
任意设置为ULONG_MAX
:https://ideone.com/u26vsi
visual-studio 对ephemient's gcc only answer 的推论是:
const auto n = 13UL;
unsigned long Index;
_BitScanReverse(&Index, n);
printf("MSB is: %u\n", Index); // Prints 3 (zero offset)
The documentation for _BitScanReverse
声明 Index
是:
加载找到的第一个设置位 (1) 的位位置
在实践中,我发现如果n
是0UL
则Index
is set to 0UL
,就像n
和1UL
一样。但是对于0UL
的n
,文档中唯一保证的是返回是:
如果没有找到设置位,则为 0
因此,与上述优选的log2
实现类似,应该检查在这种情况下将Index
设置为标记值。我在这里再次编写了一个使用 ULONG_MAX
的示例:http://rextester.com/GCU61409
【讨论】:
否,_BitScanReverse
仅在输入为 0
时返回 0。这就像x86's BSR
instruction,它只根据输入设置 ZF,而不是输出。有趣的是,当没有找到 1
位时,MS 将文档称为未设置 index
;这也与 bsr
的 x86 asm 行为相匹配。 (AMD 将其记录为在 src=0 上未修改目标寄存器,但英特尔只是说未定义的输出,即使他们的 CPU 确实实现了未修改行为。)这与 x86 的 lzcnt
不同,它给出了 32
不 -找到了。
@PeterCordes _BitScanReverse
使用从零开始的索引,因此如果 n
是 1,那么设置位的索引实际上是 0。不幸的是,正如你所说,如果 n
是 0 那么输出也是 0 :( 这意味着无法使用 return 来区分 n
是 1 还是 0。这就是我试图传达的内容。你认为有更好的说法吗?
我想你在谈论它是如何设置Index
的。这不是 return 值。如果输入为零,它将返回一个布尔值,即为 false(这就是 Index 通过引用传递而不是正常返回的原因)。 godbolt.org/g/gQKJdE。我检查了:尽管有 MS 文档的措辞,_BitScanReverse
并没有在n==0
上未设置索引:您只需获得它碰巧使用的寄存器中的任何值。 (在您的情况下,这可能与之后用于Index
的寄存器相同,导致您看到0
)。
此问题未标记为 c++。
@technosaurus 谢谢,我忘记了自己。鉴于问题是 C,我们实际上从 C99 开始就有 log2
。【参考方案10】:
假设您使用的是 x86 并且正在玩一些内联汇编程序,英特尔提供了一个 BSR
指令(“位扫描反向”)。它是 some x86s 上的 fast(在其他人上微编码)。来自手册:
在源操作数中搜索最重要的集合 位(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;
另见inline assembler tutorial,它显示(第 9.4 节)它比循环代码快得多。
【讨论】:
其实这条指令一般都是微编码成循环的,比较慢。 哪一个? BSR 还是 CNTLZ ?当我阅读上面提到的 x86-timing.pdf 时,BSR 仅在 Netburst Pentium 上很慢。不过我对 PowerPC 一无所知。 ...好的,仔细检查后会发现“BSR 仅在 P3/Pentium-M/Core2 x86s 上速度很快”。在 Netburst 和 AMD 上运行缓慢。 如果你仍然使用 GNU C,你应该使用__builtin_clz
(或__builtin_clzll
),它具有相同的未定义零行为,可以编译为单个 BSR x86。或者 LZCNT(如果可用),因为它在更多 CPU 上更快(例如,在 AMD 上,即使 BSR 很慢,它也很快,可能是因为 BSR 具有根据输入而不是结果设置 ZF 的奇怪行为)。或者目标架构上的最佳选择,因为它不限于 x86。无论如何,gcc.gnu.org/wiki/DontUseInlineAsm 什么时候可以避免它,因为它会破坏常量传播和其他一些优化。
@rlbond:嗯,P4 Prescott 上的 BSR 是 2 微指令,具有 16 个周期延迟(!),每 4c 吞吐量一个。但在早期的 Netburst 上,它只有 4 个周期延迟(仍然是 2 微指令),每 2c 吞吐量一个。 (来源:agner.org/optimize)。在大多数 CPU 上,它还依赖于 gcc 不考虑的输出(当输入为零时,实际行为是保持目标不变)。这可能会导致像***.com/questions/25078285/… 这样的问题。 IDK 为什么 gcc 在修复该问题时错过了 BSR。【参考方案11】:
这有点像寻找一种整数对数。有一些小技巧,但我已经为此制作了自己的工具。目标当然是速度。
我的认识是 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 实现有用,因为更少的 CPU 具有 SIMD lzcnt
。 x86只有这样一条指令with AVX512CD
【讨论】:
是的。由于类型别名优化,gcc 会用 -O2 对这样的代码做一些讨厌的事情。 在 x86 CPU 上整数和浮点之间的转换可能会非常昂贵 是的,FPU 成本很高。但实际时间测量表明,这比全位操作或任何循环都快。尝试并以最快的速度始终是最好的建议。不过,我对 GCC 和 -O2 没有问题。 这不是未定义的行为(通过不兼容类型的指针读取值)吗? Hacker's Delight 解释了如何在 5-3 计数前导 0 中纠正 32 位浮点数中的错误。这是他们的代码,它使用匿名联合来重叠 asFloat 和 asInt: k = k & ~(k >> 1); asFloat = (float)k + 0.5f; n = 158 - (asInt >> 23); (是的,这依赖于实现定义的行为)【参考方案12】:哇,答案太多了。我很抱歉回答一个老问题。
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 如果您查看此问题的编辑,您会看到我添加了“最佳”部分。当我更改答案时,我忘记将其删除。另外我不确定您为什么要谈论 the 面具? (什么面具?我没关注你)
((bit)mask 是用于有选择地选择/清除位的值/在&
和&~
中使用。)您可以用类似的方法替换十六进制常量((type)1<<(1<<k))-1<<(1<<k)
.
哦,对了,我用的是口罩,我完全忘记了这一点。几个月前我确实回答了这个问题...... - 嗯,因为它是在编译时评估的,所以我说它与十六进制值等效。然而,一个是神秘的,一个是十六进制的。【参考方案13】:
我知道这个问题很老了,但是我自己实现了一个 msb() 函数, 我发现这里和其他网站上介绍的大多数解决方案不一定是最有效的 - 至少就我个人对效率的定义而言(另请参阅下面的更新)。原因如下:
大多数解决方案(尤其是那些采用某种二进制搜索方案或从右到左进行线性扫描的朴素方法的解决方案)似乎忽略了这样一个事实,即对于任意二进制数,以非常长的零序列。事实上,对于任何位宽,所有整数中有一半以 1 开头,而其中四分之一以 01 开头。 看到我要说的了吗?我的论点是,从最高有效位位置到最低有效位(从左到右)开始的线性扫描并不像乍一看那样“线性”。
可以证明1,对于任何位宽,需要测试的平均位数最多为2。这转化为摊销 O(1) 相对于位数 (!) 的时间复杂度。
当然,最坏的情况还是O(n),比O(log(n))还要糟糕 您可以使用类似二分搜索的方法,但由于最坏的情况很少,因此对于大多数应用程序来说它们可以忽略不计(更新:不完全是:有可能很少,但它们发生的概率很高 - 请参阅下面的更新)。
这是我想出的“幼稚”方法,至少在我的机器上胜过大多数其他方法(32 位整数的二进制搜索方案总是需要 log2 (32) = 5 步,而这个愚蠢的算法平均需要少于 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;
更新:虽然我在这里写的对于任意整数是完全正确的,其中每个位组合都是同样可能的(我的速度测试只是测量了确定 all 32 位整数的 MSB 所需的时间,现实生活中的整数,将调用这样的函数,通常遵循不同的模式:在我的代码中,对于例如,此函数用于确定 object size 是否为 2 的幂,或查找大于或等于 object size 的下一个 2 的幂。 我的猜测是,大多数使用 MSB 的应用程序涉及的数字远小于整数可以表示的最大数字(对象大小很少使用 size_t 中的所有位)。在这种情况下,我的解决方案实际上会比二分搜索方法执行得更差 - 因此可能应该首选后者,尽管我的解决方案会更快地遍历 所有 整数。TL ;DR: 现实生活中的整数可能会偏向这个简单算法的最坏情况,这将使它最终表现更差 - 尽管它是摊销 O(1) 用于真正的任意整数。
1论证是这样的(草稿): 令 n 为位数(位宽)。共有 2n 个整数可以用 n 位表示。有 2n - 1 个以 1 开头的整数(第一个 1 是固定的,剩下的 n - 1 位可以是任何东西)。这些整数只需要一次循环即可确定 MSB。此外,有2n - 2个整数以01开头,需要2次迭代,2n - 3 sup> 以 001 开头的整数,需要 3 次迭代,依此类推。
如果我们将所有可能的整数所需的迭代次数相加,然后除以 2n(整数的总数),我们就得到所需的平均迭代次数用于确定 n 位整数的 MSB:
(1 * 2n - 1 + 2 * 2n - 2 + 3 * 2n - 3 + . .. + n) / 2n
This series of average iterations is actually convergent and has a limit of 2 for n towards infinity
因此,朴素的从左到右算法实际上对于任意数量的位。
【讨论】:
我认为 msb 函数的输入往往是均匀分布的,这并不一定是一个公平的假设。在实践中,这些输入往往是中断寄存器或位板或其他具有不均匀分布值的数据结构。对于一个公平的基准,我认为假设输出(而不是输入)将均匀分布更安全。【参考方案14】:把它放进去,因为它是“另一种”方法,似乎与已经给出的其他方法不同。
如果x==0
,则返回-1
,否则返回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];
【讨论】:
【参考方案15】:这里有一些过于复杂的答案。只有当输入已经是 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;
这应该会产生此处给出的任何软件答案的最高吞吐量,但如果您只是偶尔调用它,则更喜欢像我的第一个 sn-p 这样的无表解决方案。
【讨论】:
一些答案是无分支的,但这可能会使用条件分支进行编译。您是否只重复使用相同的值或简单的模式或其他东西进行基准测试?分支错误预测是性能的杀手。 ***.com/questions/11227809/… 我在我的国际象棋引擎中定期测试这个;此功能对位板处理的性能非常关键。是的,CPU 最终利用的有效数据集中出现了一些模式。但另一方面,我无法将超随机输入的测试视为可以优化的现实世界案例。 取决于您的功能用例。如果您正在搜索分配位图中的第一个空闲点(在找到第一个具有 != 0 或 != ~0 循环的空闲点的块之后),那可能是非常随机的。许多 ISA 有一个单一的硬件指令,它以恒定的时间运行(通常为 1 或 3 个周期延迟,单个 uop),这是一个相当高的比较标准。 (即没有编译器识别模式,__builtin_clz
与纯 C 之间存在很大差距,因为不幸的是,C 从未费心为此 CPU 操作定义标准函数。)【参考方案16】:
这是某种二进制搜索,它适用于各种(无符号!)整数类型
#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;
【讨论】:
请考虑不要将 ALL_CAPS 用于typedef
s 或除预处理器宏之外的任何内容。这是一个被广泛接受的约定。【参考方案17】:
请注意,您要做的是计算整数的整数 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);
【讨论】:
【参考方案18】:代码:
// 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)的整数部分
【讨论】:
呃呃呃呃。什么?这个功能如何?它有什么便携性吗? 窗口中的代码是可移植的。函数 FYL2X() 是一个 fpu 指令,但可以移植并可以在某些 FPU/数学库中找到。 @underscore_d 它之所以起作用,是因为浮点数已标准化...转换为双移尾数位以消除前导零,并且此代码提取指数并对其进行调整以确定移位的位数。它当然不是独立于架构的,但它可能适用于您遇到的任何机器。 这是this answer 的替代版本,有关性能和可移植性的cmets 参见那里。 (特别是用于类型双关的指针转换的不可移植性。)它使用地址数学仅重新加载double
的高 32 位,如果它确实存储/重新加载而不是类型双关,这可能很好方式,例如使用movq
指令,就像您在 x86 上可能会得到的一样。
还要注意我的[对该答案的评论],我在其中提出可怕的警告,该方法对(至少)[7FFFFFFFFFFFFE00 - 7FFFFFFFFFFFFFFF]
范围内的值给出了错误的答案.【参考方案19】:
使用逐次逼近的 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;
) 编写,更多的编译器可能会生成无分支汇编,避免每一步出现分支错误预测 (***.com/questions/11227809/…)。【参考方案20】:
正如上面的答案所指出的,有多种方法可以确定最高有效位。但是,正如还指出的那样,这些方法可能是 32 位或 64 位寄存器所独有的。 stanford.edu bithacks page 提供适用于 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;
【讨论】:
不是 int r;最初定义在#ifdef BUILD_64
标志之上?在这种情况下,它不需要在条件中重新定义。【参考方案21】:
虽然我可能只在绝对需要最佳性能时才使用这种方法(例如,用于编写某种涉及位板的棋盘游戏 AI),但最有效的解决方案是使用内联 ASM。请参阅 this blog post 的优化部分以获取带有解释的代码。
[...],
bsrl
汇编指令计算最高有效位的位置。因此,我们可以使用这个asm
声明:asm ("bsrl %1, %0" : "=r" (position) : "r" (number));
【讨论】:
扩展:标准循环解决方案(左移并检查 MSB)可能是最易读的。就像在所有涉及位旋转的情况下一样,ASM 的速度是无法超越的,尽管除非必要,否则将代码弄乱是没有意义的。黑客是一种介于两者之间的解决方案 - 以一种或另一种方式进行。 我会说取对数将是一个完全可读的解决方案(检查生成的 asm 以查看编译器是否可以对其进行优化以使用此 asm 指令) 有时内联 ASM 解决方案速度较慢,具体取决于 CPU 微码中的实现。 @rlbound:我简直不敢相信,尽管我可能弄错了。在任何现代 CPU 上,人们都会认为它会被翻译成一条指令...... @Noldorin 有点晚了,但是.. 根据定义,它是一条指令,但如果按照 rlbond 的建议对其进行微编码,则该单条指令可以在内部解码为一大堆 µops。在 AMD 的微架构和 Intel Atom 上往往是这种情况,但在普通的 Intel 微架构上,它一直是一个单一的操作。【参考方案22】:怎么样
int highest_bit(unsigned int a)
int count;
std::frexp(a, &count);
return count - 1;
?
【讨论】:
这是this answer 的一个慢(但更便携)版本,它解释了它的工作原理。【参考方案23】:Kaz Kylheku 在这里
我针对超过 63 位数字(gcc x86_64 上的 long long 类型)对两种方法进行了基准测试,远离符号位。
(你看,我碰巧需要这个“找到最高位”。)
我实现了数据驱动的二进制搜索(密切基于上述答案之一)。我还手动实现了一个完全展开的决策树,它只是带有立即操作数的代码。没有循环,没有表格。
决策树 (highest_bit_unrolled) 的基准测试速度提高了 69%,但二进制搜索具有显式测试的 n = 0 情况除外。
二分搜索对0 case的特殊测试仅比没有特殊测试的决策树快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 位!
在输入 0x3FFFFFFFFFFFFFFF 上,决策树版本比它在 1 上的速度要快很多,并且比移位器快 1120%(12.2 倍)。
我还将根据 GCC 内置函数对决策树进行基准测试,并尝试混合输入,而不是针对相同的数字重复。可能会发生一些粘滞分支预测,并且可能会出现一些不切实际的缓存场景,从而人为地使其在重复时更快。
【讨论】:
我并不是说这不好,但是您的测试程序在这里只测试相同的数字,经过 2-3 次迭代后,分支预测器将设置为它们的最终位置,然后他们将做出完美的分支预测。好消息是,在完全随机分布的情况下,一半的数字将具有接近完美的预测,即 bit63。【参考方案24】:我需要一个例程来执行此操作,在搜索网络(并找到此页面)之前,我想出了基于二分搜索的自己的解决方案。虽然我确信以前有人这样做过!它在恒定时间内运行,并且可以比发布的“明显”解决方案更快,尽管我没有提出任何伟大的主张,只是出于兴趣而发布它。
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;
【讨论】:
由于您在a == 0
时提前退出,因此else if
分支中的测试总是评估为真,因此您可以将其简化为仅else h = m;
并去掉mask
:)
(推理:你保持 [l, h) 范围内至少有一位为 1 的不变量,并且 l
【参考方案25】:
在 Josh 的基准上扩展... 可以按如下方式改进 clz
/***************** clz2 ********************/
#define NUM_OF_HIGHESTBITclz2(a) ((a) \
? (((1U) << (sizeof(unsigned)*8-1)) >> __builtin_clz(a)) \
: 0)
关于 asm:注意有 bsr 和 bsrl(这是“长”版本)。普通的可能会快一点。
【讨论】:
【参考方案26】:这里有一些(简单的)基准,目前在这个页面上给出的算法......
算法尚未针对 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;
【讨论】:
请注意,按递增顺序测试数字可能会导致在内部使用条件分支的算法从现代 CPU 中的分支预测器中获得不切实际的好处,因为附近的数字序列将产生类似的结果测试。【参考方案27】:由于 2^N 是仅设置第 N 位 (1
http://graphics.stanford.edu/~seander/bithacks.html#IntegerLogObvious
unsigned int v;
unsigned r = 0;
while (v >>= 1)
r++;
这种“显而易见”的算法可能对每个人都不是透明的,但是当您意识到代码重复右移一位直到最左边的位被移走时(请注意,C 将任何非零值视为真)并且返回班次的数量,这很有意义。这也意味着即使设置了多个位,它也可以工作——结果始终是最高有效位。
如果您在该页面上向下滚动,则会出现更快、更复杂的变化。但是,如果您知道要处理具有大量前导零的数字,那么简单的方法可能会提供可接受的速度,因为 C 中的位移相当快,而且简单的算法不需要索引数组。
注意:当使用 64 位值时,使用超智能算法时要格外小心;其中许多仅适用于 32 位值。
【讨论】:
@Johan 使用调试器单步执行有助于解释循环退出的原因。基本上,它是因为一旦最后 1 位右移,条件中的表达式的计算结果为 0(被视为假)。 使用这样的最终结果的好主意:) 注意:必须是无符号的,对于有符号整数,负数右移失败。 @Chase:不,不是。这是一个逻辑移位for unsigned。对于 signed,它可能是也可能不是是逻辑移位(实际上通常是算术移位)。 “这比 return (unsigned int)log2(val) 快 2 倍”——最微弱的赞美。【参考方案28】:这应该是闪电般的速度:
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];
【讨论】:
7 位移位,5 次或指令,多次和潜在的缓存未命中。 :) 您是否对其进行了基准测试,或者查看了生成的汇编程序?它可能最终会很慢,这取决于编译器可以消除多少。 “可能的缓存未命中”可能是由于此代码需要访问其查找表。如果调用此表时未缓存该表,则在获取该表时将出现停顿。这可能会使最坏情况下的性能比不使用 LUT 的解决方案差得多。 不是重点。它使用了比必要更多的数据缓存(甚至超过一个缓存线),以及比必要更多的指令缓存。您可能会在第一次调用该函数时获得本来可以避免的缓存未命中,并且它会不必要地污染缓存,因此在调用之后,其他代码可能会遇到比必要更多的未命中. LUT 通常不值得麻烦,因为缓存未命中很昂贵。但在我声称它“闪电般快”之前,我只是说这是我想要进行基准测试的东西。并不是说这绝对是个问题。 该表有 32 个条目,每个值 回复:have provided the only answer with source code that actually works,当unsigned
不是 32 位时,此答案会失败。不错,但不是通用的。【参考方案29】:
考虑位运算符。
我第一次误解了这个问题。您应该生成一个 int 并设置最左边的位(其他位为零)。假设 cmp 设置为该值:
position = sizeof(int)*8
while(!(n & cmp))
n <<=1;
position--;
【讨论】:
转换成字符串是什么意思? ffs 的定义接受一个 int 并返回一个 int。转换会在哪里?如果我们在单词中寻找位,那么转换的目的是什么? 我不知道这个功能。8
应该是 CHAR_BIT
。这不太可能是最快的方法,因为在退出循环时会发生分支预测错误,除非重复使用相同的输入。此外,对于小输入(很多零),它必须循环很多。这就像您在单元测试中用作易于验证的版本以与优化版本进行比较的后备方式。【参考方案30】:
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
【讨论】:
上面的代码没有回答这个问题。它返回一个无符号整数,其中 x 中的最高有效位保持打开,而所有其他位都关闭。问题是返回最高位的位置。 然后您可以使用 De Bruijn 序列方法来查找已设置位的索引。 :-) @Protagonist,他在评论中说,要么就够了。 这个(来自同一页面)可以满足您的需求,但它需要一个额外的功能。 aggregate.org/MAGIC/#Log2%20of%20an%20Integer BSR 在 Intel CPU 上速度很快,至少从 Core2 开始。 LZCNT 在 AMD CPU 上速度很快,如果使用-march=native
或其他东西启用,gcc 将它用于__builtin_clz
(因为它在每个支持它的 CPU 上都很快)。即使在 BSR“慢”的 AMD Bulldozer 系列 CPU 上,它也没有那么慢:7 m-ops 具有 4 个周期延迟和一个每 4c 吞吐量。在 Atom 上,BSR 真的很慢:16 个周期。在 Silvermont 上,它是 10 微指令,具有 10 个周期延迟。这可能比 Silvermont 上的 BSR 稍低,但 IDK。以上是关于在C中找到整数中最高设置位(msb)的最快/最有效方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章