快速查找64位整数中设置最高和最低有效位的方法

时间:2015-07-13 02:24:17

标签: c# bit-manipulation

StackOverflow上有很多关于此的问题。 很多。但是我找不到答案:

  • 适用于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,另一个是fxtbook.pdf 然而,虽然这些方法为解决问题提供了有用的方向,但它们并没有给出现成的答案。

我可以使用可重复使用的功能,只能为C#执行类似于_BitScanForward64_BitScanReverse64的操作。

6 个答案:

答案 0 :(得分:9)

在问题中链接的Bit Hacks页面上描述的其中一种方法是利用De Bruijn sequence。不幸的是,这个页面没有给出所述序列的64位版本。 This useful page解释了如何构造De Bruijn序列,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

可以在here http://javascript.daypilot.org/scheduler/找到另一篇与De Bruijn序列相关的问题中未提及的相关文章。

答案 1 :(得分:7)

.NET Core 3.0添加了BitOperations.LeadingZeroCountBitOperations.TrailingZeroCount,因此您可以直接使用它们。它们将被映射到x86的LZCNT / BSR和TZCNT / BSF指令,因此非常有效

int mostSignificantPosition = 63 - BitOperations.LeadingZeroCount(0x1234L);
int leastSignificantPosition = BitOperations.TrailingZeroCount(0x1234L);

答案 2 :(得分:5)

根据我的评论,这是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;
}

答案 3 :(得分:2)

最快获取IL代码最高有效位的方法应该是float转换并访问指数位。

保存代码:

int myint = 7;
int msb = (BitConverter.SingleToInt32Bits(myint) >> 23) - 0x7f;

更快的方法是使用msblsb CPU指令。正如phuclv所提到的那样,它在.Net Core 3.0中可用,因此我添加了一个测试,不幸的是,该测试的速度并不快。

此处要求提供uintulong的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

答案 4 :(得分: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)使其意图更明显。
  • 适应不同的字长现在应该是显而易见且微不足道的。
  • 无需特殊处理(输入== 0),将其删除将加快所有其他输入的速度。
  • 将int用于计数器比使用UInt64更合理。 (甚至可以将其设置为一个字节,但是int是默认的整数类型,并且据信对于每个平台来说都是最快的。)
  • 为主动内联添加了属性,以确保最佳性能。

在运行时无需计算任何“(bits-x)”,因此编译器应对其进行预计算。因此,提高可读性是免费的。

编辑:如@Peter Cordes所指出的,您应该只使用System.Numerics.BitOperations.LeadingZeroCount 如果有可用的BitOperations类。我,其中一个经常没有。

答案 5 :(得分:0)

由于我们在这里谈论的是 .NET ,通常最好不要使用 进行外部本地调用。但是,如果您可以忍受每个操作的托管/非托管往返的开销,则以下两个调用提供了对本机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