在 64 位整数中查找最高和最低有效位集的快速方法
Fast way of finding most and least significant bit set in a 64-bit integer
Whosebug 上有很多关于此的问题。 很多。但是我找不到答案:
- 在 C# 中工作
- 适用于 64 位整数(相对于 32 位)
快于:
private static int Obvious(ulong v)
{
int r = 0;
while ((v >>= 1) != 0)
{
r++;
}
return r;
}
甚至
int r = (int)(Math.Log(v,2));
我这里假设是 64 位 Intel CPU。
一个有用的参考是 Bit Hacks page and another is fxtbook.pdf
然而,虽然这些提供了解决问题的有用方向,但它们并没有给出现成的答案。
我正在寻找一个可重复使用的函数,它可以做类似于 _BitScanForward64 and _BitScanReverse64 的事情,仅适用于 C#。
在问题中链接的 Bit Hacks 页面上描述的其中一种方法是利用 De Bruijn sequence. Unfortunately this page does not give a 64-bit version of said sequence. This useful page explains how De Bruijn sequences can be constructed, and this one 给出了一个用 C++ 编写的序列生成器的示例。如果我们修改给定的代码,我们可以生成多个序列,其中之一在下面的 C# 代码中给出:
public static class BitScanner
{
private const ulong Magic = 0x37E84A99DAE458F;
private static readonly int[] MagicTable =
{
0, 1, 17, 2, 18, 50, 3, 57,
47, 19, 22, 51, 29, 4, 33, 58,
15, 48, 20, 27, 25, 23, 52, 41,
54, 30, 38, 5, 43, 34, 59, 8,
63, 16, 49, 56, 46, 21, 28, 32,
14, 26, 24, 40, 53, 37, 42, 7,
62, 55, 45, 31, 13, 39, 36, 6,
61, 44, 12, 35, 60, 11, 10, 9,
};
public static int BitScanForward(ulong b)
{
return MagicTable[((ulong) ((long) b & -(long) b)*Magic) >> 58];
}
public static int BitScanReverse(ulong b)
{
b |= b >> 1;
b |= b >> 2;
b |= b >> 4;
b |= b >> 8;
b |= b >> 16;
b |= b >> 32;
b = b & ~(b >> 1);
return MagicTable[b*Magic >> 58];
}
}
我还将序列生成器的 C# 端口发布到 github
问题中未提及的另一篇相关文章具有 De Bruijn 序列的良好封面,可以找到 here。
根据我的评论,这是 C# 中的一个函数,用于计算为 64 位整数修改的前导零位。
public static UInt64 CountLeadingZeros(UInt64 input)
{
if (input == 0) return 64;
UInt64 n = 1;
if ((input >> 32) == 0) { n = n + 32; input = input << 32; }
if ((input >> 48) == 0) { n = n + 16; input = input << 16; }
if ((input >> 56) == 0) { n = n + 8; input = input << 8; }
if ((input >> 60) == 0) { n = n + 4; input = input << 4; }
if ((input >> 62) == 0) { n = n + 2; input = input << 2; }
n = n - (input >> 63);
return n;
}
更新:
如果您使用的是较新版本的 C#,请根据以下答案检查这是否是内置的。
既然我们在这里谈论 .NET,通常 而不是 求助于外部本机调用。但是,如果您可以忍受每个操作的 managed/unmanaged 往返开销,则以下两个调用提供了对本机 CPU 指令的非常直接和纯粹的访问。
还显示了 ntdll.dll
中各个完整函数的(简约)反汇编。该库将出现在任何 Windows 机器上,并且如果如图所示引用,将始终可以找到。
最低有效位 (LSB):
[DllImport("ntdll"), SuppressUnmanagedCodeSecurity]
public static extern int RtlFindLeastSignificantBit(ulong ul);
// X64:
// bsf rdx, rcx
// mov eax, 0FFFFFFFFh
// movzx ecx, dl
// cmovne eax,ecx
// ret
最高有效位 (MSB):
[DllImport("ntdll"), SuppressUnmanagedCodeSecurity]
public static extern int RtlFindMostSignificantBit(ulong ul);
// X64:
// bsr rdx, rcx
// mov eax, 0FFFFFFFFh
// movzx ecx, dl
// cmovne eax,ecx
// ret
用法:
这是一个用法示例,它要求上述声明是可访问的。再简单不过了。
int ix;
ix = RtlFindLeastSignificantBit(0x00103F0A042C1D80UL); // ix --> 7
ix = RtlFindMostSignificantBit(0x00103F0A042C1D80UL); // ix --> 52
在 IL 代码中获取最高有效位的最快方法应该是 float
转换和访问指数位。
保存代码:
int myint = 7;
int msb = (BitConverter.SingleToInt32Bits(myint) >> 23) - 0x7f;
更快的方法是 msb
和 lsb
cpu 指令。正如 phuclv 所提到的,它在 .Net Core 3.0 上可用,所以我添加了一个测试,不幸的是它并没有快多少。
此处要求的是 uint
和 ulong
的 10000 个隐蔽层的 BenchmarkDotNet 结果。加速是 2 倍,因此 BitScanner 解决方案速度很快,但无法击败本机浮点数转换。
Method | Mean | Error | StdDev | Ratio
BitScannerForward | 34.37 us | 0.420 us | 0.372 us | 1.00
BitConverterULong | 18.59 us | 0.238 us | 0.223 us | 0.54
BitConverterUInt | 18.58 us | 0.129 us | 0.121 us | 0.54
NtdllMsbCall | 31.34 us | 0.204 us | 0.170 us | 0.91
LeadingZeroCount | 15.85 us | 0.169 us | 0.150 us | 0.48
.NET Core 3.0 添加了 BitOperations.LeadingZeroCount and BitOperations.TrailingZeroCount 以便您可以直接使用它们。它们将被映射到 x86 的 LZCNT/BSR 和 TZCNT/BSF 指令,因此非常高效
int mostSignificantPosition = 63 - BitOperations.LeadingZeroCount(0x1234L);
int leastSignificantPosition = BitOperations.TrailingZeroCount(0x1234L);
或者最高有效位的位置可以这样计算
int mostSignificantPosition = BitOperations.Log2(x - 1) + 1
@Taekahn 给出了很好的答案。我会稍微改进一下:
[System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int CountLeadingZeros(this ulong input)
{
const int bits = 64;
// if (input == 0L) return bits; // Not needed. Use only if 0 is very common.
int n = 1;
if ((input >> (bits - 32)) == 0) { n += 32; input <<= 32; }
if ((input >> (bits - 16)) == 0) { n += 16; input <<= 16; }
if ((input >> (bits - 8)) == 0) { n += 8; input <<= 8; }
if ((input >> (bits - 4)) == 0) { n += 4; input <<= 4; }
if ((input >> (bits - 2)) == 0) { n += 2; input <<= 2; }
return n - (int)(input >> (bits - 1));
}
- 避免使用稍微神奇的数字,而是使用 (bits - x) 使他们的意图更加明显。
- 适应不同的字长现在应该是显而易见且微不足道的。
- 将 (input == 0) 视为特殊是不必要的,删除它会加速所有其他输入。
- 使用int作为计数器比使用UInt64更合理。 (甚至可以将它变成一个字节,但 int 是默认的整数类型,并且据说对于每个平台来说都是最快的。)
- 为积极内联添加了属性,以确保最佳性能。
不需要在运行时计算任何“(bits - x)”,因此编译器应该预先计算它们。因此,可读性的提高是免费的。
编辑:正如@Peter Cordes 所指出的,如果 BitOperations class 可用,您应该只使用System.Numerics.BitOperations.LeadingZeroCount 。我,一方面,经常不这样做。
Whosebug 上有很多关于此的问题。 很多。但是我找不到答案:
- 在 C# 中工作
- 适用于 64 位整数(相对于 32 位)
快于:
private static int Obvious(ulong v)
{
int r = 0;
while ((v >>= 1) != 0)
{
r++;
}
return r;
}
甚至
int r = (int)(Math.Log(v,2));
我这里假设是 64 位 Intel CPU。
一个有用的参考是 Bit Hacks page and another is fxtbook.pdf 然而,虽然这些提供了解决问题的有用方向,但它们并没有给出现成的答案。
我正在寻找一个可重复使用的函数,它可以做类似于 _BitScanForward64 and _BitScanReverse64 的事情,仅适用于 C#。
在问题中链接的 Bit Hacks 页面上描述的其中一种方法是利用 De Bruijn sequence. Unfortunately this page does not give a 64-bit version of said sequence. This useful page explains how De Bruijn sequences can be constructed, and this one 给出了一个用 C++ 编写的序列生成器的示例。如果我们修改给定的代码,我们可以生成多个序列,其中之一在下面的 C# 代码中给出:
public static class BitScanner
{
private const ulong Magic = 0x37E84A99DAE458F;
private static readonly int[] MagicTable =
{
0, 1, 17, 2, 18, 50, 3, 57,
47, 19, 22, 51, 29, 4, 33, 58,
15, 48, 20, 27, 25, 23, 52, 41,
54, 30, 38, 5, 43, 34, 59, 8,
63, 16, 49, 56, 46, 21, 28, 32,
14, 26, 24, 40, 53, 37, 42, 7,
62, 55, 45, 31, 13, 39, 36, 6,
61, 44, 12, 35, 60, 11, 10, 9,
};
public static int BitScanForward(ulong b)
{
return MagicTable[((ulong) ((long) b & -(long) b)*Magic) >> 58];
}
public static int BitScanReverse(ulong b)
{
b |= b >> 1;
b |= b >> 2;
b |= b >> 4;
b |= b >> 8;
b |= b >> 16;
b |= b >> 32;
b = b & ~(b >> 1);
return MagicTable[b*Magic >> 58];
}
}
我还将序列生成器的 C# 端口发布到 github
问题中未提及的另一篇相关文章具有 De Bruijn 序列的良好封面,可以找到 here。
根据我的评论,这是 C# 中的一个函数,用于计算为 64 位整数修改的前导零位。
public static UInt64 CountLeadingZeros(UInt64 input)
{
if (input == 0) return 64;
UInt64 n = 1;
if ((input >> 32) == 0) { n = n + 32; input = input << 32; }
if ((input >> 48) == 0) { n = n + 16; input = input << 16; }
if ((input >> 56) == 0) { n = n + 8; input = input << 8; }
if ((input >> 60) == 0) { n = n + 4; input = input << 4; }
if ((input >> 62) == 0) { n = n + 2; input = input << 2; }
n = n - (input >> 63);
return n;
}
更新:
如果您使用的是较新版本的 C#,请根据以下答案检查这是否是内置的。
既然我们在这里谈论 .NET,通常 而不是 求助于外部本机调用。但是,如果您可以忍受每个操作的 managed/unmanaged 往返开销,则以下两个调用提供了对本机 CPU 指令的非常直接和纯粹的访问。
还显示了 ntdll.dll
中各个完整函数的(简约)反汇编。该库将出现在任何 Windows 机器上,并且如果如图所示引用,将始终可以找到。
最低有效位 (LSB):
[DllImport("ntdll"), SuppressUnmanagedCodeSecurity]
public static extern int RtlFindLeastSignificantBit(ulong ul);
// X64:
// bsf rdx, rcx
// mov eax, 0FFFFFFFFh
// movzx ecx, dl
// cmovne eax,ecx
// ret
最高有效位 (MSB):
[DllImport("ntdll"), SuppressUnmanagedCodeSecurity]
public static extern int RtlFindMostSignificantBit(ulong ul);
// X64:
// bsr rdx, rcx
// mov eax, 0FFFFFFFFh
// movzx ecx, dl
// cmovne eax,ecx
// ret
用法:
这是一个用法示例,它要求上述声明是可访问的。再简单不过了。
int ix;
ix = RtlFindLeastSignificantBit(0x00103F0A042C1D80UL); // ix --> 7
ix = RtlFindMostSignificantBit(0x00103F0A042C1D80UL); // ix --> 52
在 IL 代码中获取最高有效位的最快方法应该是 float
转换和访问指数位。
保存代码:
int myint = 7;
int msb = (BitConverter.SingleToInt32Bits(myint) >> 23) - 0x7f;
更快的方法是 msb
和 lsb
cpu 指令。正如 phuclv 所提到的,它在 .Net Core 3.0 上可用,所以我添加了一个测试,不幸的是它并没有快多少。
此处要求的是 uint
和 ulong
的 10000 个隐蔽层的 BenchmarkDotNet 结果。加速是 2 倍,因此 BitScanner 解决方案速度很快,但无法击败本机浮点数转换。
Method | Mean | Error | StdDev | Ratio
BitScannerForward | 34.37 us | 0.420 us | 0.372 us | 1.00
BitConverterULong | 18.59 us | 0.238 us | 0.223 us | 0.54
BitConverterUInt | 18.58 us | 0.129 us | 0.121 us | 0.54
NtdllMsbCall | 31.34 us | 0.204 us | 0.170 us | 0.91
LeadingZeroCount | 15.85 us | 0.169 us | 0.150 us | 0.48
.NET Core 3.0 添加了 BitOperations.LeadingZeroCount and BitOperations.TrailingZeroCount 以便您可以直接使用它们。它们将被映射到 x86 的 LZCNT/BSR 和 TZCNT/BSF 指令,因此非常高效
int mostSignificantPosition = 63 - BitOperations.LeadingZeroCount(0x1234L);
int leastSignificantPosition = BitOperations.TrailingZeroCount(0x1234L);
或者最高有效位的位置可以这样计算
int mostSignificantPosition = BitOperations.Log2(x - 1) + 1
@Taekahn 给出了很好的答案。我会稍微改进一下:
[System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int CountLeadingZeros(this ulong input)
{
const int bits = 64;
// if (input == 0L) return bits; // Not needed. Use only if 0 is very common.
int n = 1;
if ((input >> (bits - 32)) == 0) { n += 32; input <<= 32; }
if ((input >> (bits - 16)) == 0) { n += 16; input <<= 16; }
if ((input >> (bits - 8)) == 0) { n += 8; input <<= 8; }
if ((input >> (bits - 4)) == 0) { n += 4; input <<= 4; }
if ((input >> (bits - 2)) == 0) { n += 2; input <<= 2; }
return n - (int)(input >> (bits - 1));
}
- 避免使用稍微神奇的数字,而是使用 (bits - x) 使他们的意图更加明显。
- 适应不同的字长现在应该是显而易见且微不足道的。
- 将 (input == 0) 视为特殊是不必要的,删除它会加速所有其他输入。
- 使用int作为计数器比使用UInt64更合理。 (甚至可以将它变成一个字节,但 int 是默认的整数类型,并且据说对于每个平台来说都是最快的。)
- 为积极内联添加了属性,以确保最佳性能。
不需要在运行时计算任何“(bits - x)”,因此编译器应该预先计算它们。因此,可读性的提高是免费的。
编辑:正如@Peter Cordes 所指出的,如果 BitOperations class 可用,您应该只使用System.Numerics.BitOperations.LeadingZeroCount 。我,一方面,经常不这样做。