log2(int)和log2(float)的最快实现方式

29

问题是

是否有其他(更快的)基本2log实现?

应用

log2(int)和log2(float)操作在许多不同的情境下非常有用,例如压缩算法、3D引擎和机器学习等。在几乎所有这些情境中,它们都用于被调用数十亿次的低级代码...尤其是log2(int)操作非常有用。

因为我经常使用log2,所以我不想在这里给出我正在工作的具体应用程序。相同的是事实上这是一个真正的性能损耗(如各种应用程序的性能测试所示)。对我来说,关键是尽可能快地完成这项任务。

完整的源代码以测试所有实现都附在底部,所以您可以自行查看。

当然...至少运行3次测试,并确保计数器足够大以达到多秒钟。此外,我进行'add'操作以确保整个循环不会被JIT'ter神奇地删除。那么,让我们开始真正的工作。

简单实现

C#中一个简单的2log实现是:

(int)(Math.Log(x) / Math.Log(2))

这个实现方式很简单,但速度非常慢。它需要2次Log运算,而Log本身就相当缓慢。当然,我们可以通过将1.0/Math.Log(2)设置为一个常量来进行优化。

请注意,我们需要略微修改这个常量以获得正确的结果(由于浮点误差),或者添加一个小数以获得正确的结果。我选择了后者,但实际上并不重要-最终结果在所有情况下都很慢。

表查找

一个更快的解决方案是使用查找表。虽然您可以使用2的任何次幂的查找表,但我通常使用256个或64K个条目的表格大小。

首先,我们创建查找表:

lookup = new int[256];
for (int i = 1; i < 256; ++i)
{
    lookup[i] = (int)(Math.Log(i) / Math.Log(2));
}

接下来,我们按照以下步骤实现2log:

private static int LogLookup(int i)
{
    if (i >= 0x1000000) { return lookup[i >> 24] + 24; }
    else if (i >= 0x10000) { return lookup[i >> 16] + 16; }
    else if (i >= 0x100) { return lookup[i >> 8] + 8; }
    else { return lookup[i]; }
}

正如您所看到的,表格查找是一种更快的实现方式,但缺点是不能用来计算log2(float)

分支去除

众所周知,处理器不擅长执行分支操作,因此我认为可以通过消除分支来改进表格查找。我引入了第二个表格,其中包含值,并移位以查找表格中的条目,而不是使用很多if语句。

nobranch = new int[16] { 0, 0, 8, 8, 16, 16, 16, 16, 24, 24, 24, 24, 24, 24, 24, 24 };

private static int LogDoubleLookup(int i)
{
    int n = (i | (i >> 4));
    n = (n | (n >> 2));
    n = (n | (n >> 1));
    n = ((n & 0x1000000) >> 21) | ((n & 0x10000) >> 14) | ((n & 0x100) >> 7) | (n & 1);
    int br = nobranch[n];
    return lookup[i >> br] + br;
}

如果你运行此测试,你会发现它比if-then-else的解决方案更慢。

接着是英特尔80386

英特尔多年前就认识到这是一个重要操作,因此他们在处理器中实现了位扫描前进(BSF)。其他处理器也有类似的指令。这绝对是我知道的做2log最快的方法——但不幸的是,我不知道如何从C#中直接使用这些好用的函数……我不喜欢有一种实现,当新的平板电脑或手机问世时就不能用了,而且我不知道任何跨平台的解决方案能让我直接使用这个函数。

其他的实现

正如l4V所指出的(谢谢!),还有其他一些实现,具体包括:

  • Trivial loop. 我省略了这个因为它很平凡,也不太快。实现在TestTrivial中。
  • 可以使用的64位IEEE / int联合。实现在TestFloat
  • DeBruijn查找表。实现在TestDeBruijn
  • 二分查找。实现在TestBinary

除了我喜欢这个名字,DeBruijn查找表与普通查找表一样快,使其成为这里最快的算法之一......我尝试过的所有其他算法都要慢得多。

完整测试代码

public class Log2Test
{
    public static void TestNaive()
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        int n = 0;
        for (int i = 1; i < 100000000; ++i)
        {
            n += (int)(Math.Log(i) / Math.Log(2.0));
        }
        sw.Stop();
        Console.WriteLine("Result: {0} - naive implementation took {1:0.000}s", n, sw.Elapsed.TotalSeconds);
    }

    public static int LogTrivialLoop(int v)
    {
        int r = 0;
        while ((v >>= 1) > 0) // unroll for more speed...
        {
            r++;
        }
        return r;
    }

    public static void TestTrivialLoop()
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        int n = 0;
        for (int i = 1; i < 100000000; ++i)
        {
            n += LogTrivialLoop(i);
        }
        sw.Stop();
        Console.WriteLine("Result: {0} - loop implementation took {1:0.000}s", n, sw.Elapsed.TotalSeconds);
    }

    public static int LogFloat(int v)
    {
        Helper h = new Helper() { U1 = v, U2 = 0x43300000 };
        h.D -= 4503599627370496.0;
        return (h.U2 >> 20) - 0x3FF;
    }

    public static void TestFloat()
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        int n = 0;
        for (int i = 1; i < 100000000; ++i)
        {
            n += LogFloat(i);
        }
        sw.Stop();
        Console.WriteLine("Result: {0} - IEEE float implementation took {1:0.000}s", n, sw.Elapsed.TotalSeconds);
    }

    [StructLayout(LayoutKind.Explicit)]
    private struct Helper
    {
        [FieldOffset(0)]
        public int U1;
        [FieldOffset(4)]
        public int U2;
        [FieldOffset(0)]
        public double D;
    }

    public static void TestConstant()
    {
        double c = 1.0 / Math.Log(2.0);
        Stopwatch sw = new Stopwatch();
        sw.Start();
        int n = 0;
        for (int i = 1; i < 100000000; ++i)
        {
            n += (int)(0.00000000001 + Math.Log(i) * c);
        }
        sw.Stop();
        Console.WriteLine("Result: {0} - naive 2 implementation took {1:0.000}s", n, sw.Elapsed.TotalSeconds);
    }

    private static int LogLookup(int i)
    {
        if (i >= 0x1000000) { return lookup[i >> 24] + 24; }
        else if (i >= 0x10000) { return lookup[i >> 16] + 16; }
        else if (i >= 0x100) { return lookup[i >> 8] + 8; }
        else { return lookup[i]; }
    }

    public static void TestLookup()
    {
        lookup = new int[256];
        for (int i = 1; i < 256; ++i)
        {
            lookup[i] = (int)(Math.Log(i) / Math.Log(2));
        }
        Stopwatch sw = new Stopwatch();
        sw.Start();
        int n = 0;
        for (int i = 1; i < 100000000; ++i)
        {
            n += LogLookup(i);
        }
        sw.Stop();
        Console.WriteLine("Result: {0} - table lookup implementation took {1:0.000}s", n, sw.Elapsed.TotalSeconds);
    }

    private static int LogDoubleLookup(int i)
    {
        int n = (i | (i >> 4));
        n = (n | (n >> 2));
        n = (n | (n >> 1));
        n = ((n & 0x1000000) >> 21) | ((n & 0x10000) >> 14) | ((n & 0x100) >> 7) | (n & 1);
        int br = nobranch[n];
        return lookup[i >> br] + br;
    }

    public static void TestDoubleLookup()
    {
        // Lookup table was already constructed earlier
        Stopwatch sw = new Stopwatch();
        sw.Start();
        int n = 0;
        for (int i = 1; i < 100000000; ++i)
        {
            n += LogDoubleLookup(i);
        }
        sw.Stop();
        Console.WriteLine("Result: {0} - double table lookup implementation took {1:0.000}s", n, sw.Elapsed.TotalSeconds);
    }

    private static int LogBinary(int v)
    {
        /* This is the worst implementation ever... - apparently C# is a slow-branching language

        int[] b = { 0x2, 0xC, 0xF0, 0xFF00, 0x7FFF0000 };
        int[] S = { 1, 2, 4, 8, 16 };

        int r = 0; // result of log2(v) will go here
        for (int i = 4; i >= 0; i--) // unroll for speed...
        {
            if ((v & b[i]) != 0)
            {
                v >>= S[i];
                r |= S[i];
            }
        }
        return r;

         */

        int r = (((v > 0xFFFF)) ? 0x10 : 0); 
        v >>= r;
        int shift = ((v > 0xFF) ? 0x8 : 0); 
        v >>= shift; 
        r |= shift;
        shift = ((v > 0xF) ? 0x4 : 0); 
        v >>= shift;
        r |= shift;
        shift = ((v > 0x3) ? 0x2 : 0); 
        v >>= shift;
        r |= shift;
        r |= (v >> 1);
        return r;
    }

    public static void TestBinary()
    {
        // Lookup table was already constructed earlier
        Stopwatch sw = new Stopwatch();
        sw.Start();
        int n = 0;
        for (int i = 1; i < 100000000; ++i)
        {
            n += LogBinary(i);
        }
        sw.Stop();
        Console.WriteLine("Result: {0} - binary search implementation took {1:0.000}s", n, sw.Elapsed.TotalSeconds);
    }

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

    private static int LogDeBruijn(int v)
    {
        v |= v >> 1; // first round down to one less than a power of 2 
        v |= v >> 2;
        v |= v >> 4;
        v |= v >> 8;
        v |= v >> 16;

        return MultiplyDeBruijnBitPosition[(uint)(v * 0x07C4ACDDU) >> 27];
    }

    public static void TestDeBruijn()
    {
        // Lookup table was already constructed earlier
        Stopwatch sw = new Stopwatch();
        sw.Start();
        int n = 0;
        for (int i = 1; i < 100000000; ++i)
        {
            n += LogDeBruijn(i);
        }
        sw.Stop();
        Console.WriteLine("Result: {0} - de Bruijn implementation took {1:0.000}s", n, sw.Elapsed.TotalSeconds);
    }

    private static int[] lookup;
    private static readonly int[] nobranch = new int[16] { 0, 0, 8, 8, 16, 16, 16, 16, 24, 24, 24, 24, 24, 24, 24, 24 };

    static void Main(string[] args)
    {
        TestConstant();
        TestNaive();
        TestDeBruijn();
        TestBinary();
        TestFloat();
        TestTrivialLoop();
        TestLookup();
        TestDoubleLookup();
        Console.ReadLine();
    }
}

1
http://graphics.stanford.edu/~seander/bithacks.html#IntegerLogObvious - I4V
@I4V,这是一个很棒的答案。 - Ben
也许你可以编写自己的日志算法Log(e,X)来获取最快的解决方案。 - WiiMaxx
我知道这是一个相当老的帖子,但我想知道如何在浮点输入中使用它们?我正在尝试找到0-1之间数字的log2;我能否利用这些方法来处理浮点数? - dmarra
在我的经验里,很难打败那个标准的(C ++)实现。我能想到的唯一可能起作用的事情是使用多项式估算算法,并使用 AVX 指令同时为向量计算对数...即使对于这个,我也不确定。 - atlaste
显示剩余3条评论
9个回答

5

已经提到的二进制解决方案被采用并去除了分支。进行了一些测试,结果表明它比DeBruijn快1.3倍。

public static int Log2(int v)
{
    int r = 0xFFFF - v >> 31 & 0x10;
    v >>= r;
    int shift = 0xFF - v >> 31 & 0x8;
    v >>= shift; 
    r |= shift;
    shift = 0xF - v >> 31 & 0x4;
    v >>= shift;
    r |= shift;
    shift = 0x3 - v >> 31 & 0x2;
    v >>= shift;
    r |= shift;
    r |= (v >> 1);
    return r;
}

非常有趣;我之前用C++创建了完全相同的东西(可能是使用AVX2指令集),结果变得更慢了。等我有空时,我会进一步研究一下这个问题,谢谢。 - atlaste
在我的C# 4.0基准测试中,这种方法的性能比atlaste答案中的int LogDeBruijn(int v)慢了20%(我也发现这是最好的方法)。 - Special Sauce
@SpecialSauce,你是把算法放在for循环内部还是放在被for循环调用的方法中?我通常会把要测试的算法放在for循环内部,因为在C#中方法调用速度较慢,而我不是在测试方法调用。另外,你确定已经在Release模式下进行了测试吗? - DanielSig

3

这里有一些整数算法

在C#中:

public static uint FloorLog2(uint x)
{
    x |= (x >> 1);
    x |= (x >> 2);
    x |= (x >> 4);
    x |= (x >> 8);
    x |= (x >> 16);

    return (uint)(NumBitsSet(x) - 1);
}

public static uint CeilingLog2(uint x)
{
    int y = (int)(x & (x - 1));

    y |= -y;
    y >>= (WORDBITS - 1);
    x |= (x >> 1);
    x |= (x >> 2);
    x |= (x >> 4);
    x |= (x >> 8);
    x |= (x >> 16);

    return (uint)(NumBitsSet(x) - 1 - y);
}

public static int NumBitsSet(uint x)
{
    x -= ((x >> 1) & 0x55555555);
    x = (((x >> 2) & 0x33333333) + (x & 0x33333333));
    x = (((x >> 4) + x) & 0x0f0f0f0f);
    x += (x >> 8);
    x += (x >> 16);

    return (int)(x & 0x0000003f);
}

private const int WORDBITS = 32;

您应该查看我提供链接的网站上的原始代码,特别是关于Log2(0)发生了什么。


谢谢。我已经尝试了FloorLog2,因为它类似于我的操作。然而,我担心发现该算法比DeBruijn和单次查找的解决方案慢了一倍... - atlaste
有趣的 - 好吧,这次尝试是值得的! :) - Matthew Watson
1
当然,谢谢。我也很惊讶地发现,在C#中,表查找通常比其他方法更快...不过,我有点难过的是,自1985年以来,几乎所有CPU都有一条指令可以在不计算任何东西的情况下完成所有工作...似乎这只是在你转向“高级语言”时才会失去的东西... - atlaste
@StefandeBruijn,使用P/Invoke怎么样? - Display Name
在我的测试中,这比其他选择都要慢。 - atlaste

2
inline int fast_log2(register double x)
{ 
    return (reinterpret_cast<uint64_t&>(x) >> 52) - 1023;
};

2

如果需要更多的算法,请查看这里:http://www.asmcommunity.net/forums/topic/?id=15010

此外,我在C++中进行了一些测试,我的BSR实现比查找表慢。

  • 我正在使用BDS2006,通过asm指令进行状态推送/弹出可能会导致减速。
  • 您的查找表很好,但我使用的是11位表而不是8位。
  • 它将32位分为3个分支而不是4个。
  • 而且它还小到足以处理而无需初始化函数。

代码:

//---------------------------------------------------------------------------
DWORD log2_slow(const DWORD &x)
    {
    DWORD m,i;
    if (!x) return 0;
    if (x>=0x80000000) return 31;
    for (m=1,i=0;m<x;m<<=1,i++);
     if (m!=x) i--;
    return i;
    }
//---------------------------------------------------------------------------
DWORD log2_asm(const DWORD &x)
    {
    DWORD xx=x;
    asm {
        mov eax,xx
        bsr eax,eax;
        mov xx,eax;
        }
    return xx;
    }
//---------------------------------------------------------------------------
BYTE _log2[2048]=
    {
     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,
     8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
     8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
     9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
     9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
     9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
     9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
    10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,
    10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,
    10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,
    10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,
    10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,
    10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,
    10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,
    10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,
    };
DWORD log2(const DWORD &x)
    {
         if (x>=0x00400000) return _log2[x>>22]+22;
    else if (x>=0x00000800) return _log2[x>>11]+11;
    else                    return _log2[x];
    }
//---------------------------------------------------------------------------

测试代码:

DWORD x,j,i,n=256;
tbeg(); for (i=0;i<32;i++) for (j=0;j<n;j++) x=log2     (j<<i); tend(); mm_log->Lines->Add(tstr(1));
tbeg(); for (i=0;i<32;i++) for (j=0;j<n;j++) x=log2_asm (j<<i); tend(); mm_log->Lines->Add(tstr(1));
tbeg(); for (i=0;i<32;i++) for (j=0;j<n;j++) x=log2_slow(j<<i); tend(); mm_log->Lines->Add(tstr(1));

我的AMD A8-5500 3.2 GHz的测试结果:

[   0.040 ms] log2     (x) - 11bit lookup table
[   0.060 ms] log2_asm (x) - BSR
[   0.415 ms] log2_slow(x) - shift loop

注意:

  • 由于使用DWORDS,log2(0) -> 0,实际上应该为-inf。
  • 对于所有函数,其他值都是正确的。

BSR 在 AMD 上非常慢(在 Intel 上很快)。 - harold
可能存在asm指令导致减速的情况...我已经碰到过很多次了(除非asm的速度优势超越了那个状态推入/弹出的问题,否则asm实现比C++慢)。目前我只能通过asm加速32位uint操作中的乘法和除模运算。 - Spektre
你也可以尝试使用declspec(naked)或链接的普通asm来实现它,你能试试吗? - harold
我还会确保测试DeBruijn方法;它所需的数据非常少,可能适合缓存行 - 这可能会给您带来更好的结果。 - atlaste

2

干净、可靠、快速!
(需要 .NET Core 3 或更高版本)

int val = BitOperations.Log2(x);

2

已经有很多答案提供了快速近似计算log2(int)的方法,但是对于log2(float)的方法较少,因此这里提供两种(给出Java实现),它们都使用查找表和尾数/位操作:

快速准确计算log2(float):

/**
 * Calculate the logarithm to base 2, handling special cases.
 */
public static float log2(float x) {

    final int bits = Float.floatToRawIntBits(x);
    final int e = (bits >> 23) & 0xff;
    final int m = (bits & 0x7fffff);

    if (e == 255) {
        if (m != 0) {
            return Float.NaN;
        }
        return ((bits >> 31) != 0) ? Float.NaN : Float.POSITIVE_INFINITY;
    }

    if ((bits >> 31) != 0) {
        return (e == 0 && m == 0) ? Float.NEGATIVE_INFINITY : Float.NaN;
    }

    return (e == 0 ? data[m >>> qm1] : e + data[((m | 0x00800000) >>> q)]);
}

注意:

  • 如果参数为NaN或小于零,则结果为NaN。
  • 如果参数为正无穷大,则结果为正无穷大。
  • 如果参数为正零或负零,则结果为负无穷大。

快速准确的log2(float) (稍微快一些,不进行检查)

/**
 * Calculate the logarithm using base 2. Requires the argument be finite and
 * positive.
 */
public static float fastLog2(float x) {
    final int bits = Float.floatToRawIntBits(x);
    final int e = (bits >> 23) & 0xff;
    final int m = (bits & 0x7fffff);
    return (e == 0 ? data[m >>> qm1] : e + data[((m | 0x00800000) >>> q)]);
}

这种第二种方法放弃了其他方法中的检查,因此有以下特殊情况:
  • 如果参数为NaN,则结果不正确。
  • 如果参数为负数,则结果不正确。
  • 如果参数为正无穷大,则结果不正确。
  • 如果参数为正零或负零,则结果为负无穷大。

这两种方法都依赖于查找表data(以及变量qqm1)。以下方法用于填充这些内容。 n定义了精度和空间权衡。
static int q, qm1;
static float[] data;

/**
 * Compute lookup table for a given base table size.
 * 
 * @param n The number of bits to keep from the mantissa. Table storage =
 *          2^(n+1) * 4 bytes, e.g. 64Kb for n=13. Must be in the range
 *          0<=n<=23
 */
public static void populateLUT(int n) {

    final int size = 1 << (n + 1);

    q = 23 - n;
    qm1 = q - 1;
    data = new float[size];

    for (int i = 0; i < size; i++) {
        data[i] = (float) (Math.log(i << q) / Math.log(2)) - 150;
    }
}

populateLUT(12);
log2(6666); // = 12.702606

2
另一个log2(int)函数:(不再是最快的)
    [StructLayout(LayoutKind.Explicit)]
    private struct ConverterStruct
    {
        [FieldOffset(0)] public int asInt;
        [FieldOffset(0)] public float asFloat;
    }

    public static int Log2(uint val)
    {
        ConverterStruct a;  a.asInt = 0; a.asFloat = val;
        return ((a.asInt >> 23 )+ 1) & 0x1F;
    }

注: 将指数用于浮点数的灵感来自于SPWorley 3/22/2009。在生产代码中使用时要小心,因为这会在不是小端字节序的体系结构上失败。
如果您想要一个“字节序”安全的东西,请查看spender 5/3/2012。它还支持零。
最快的是新的内置BitOperations.Log2(x) 以下是一些基准测试:(代码在此处:https://github.com/SunsetQuest/Fast-Integer-Log2
Function               Time1  Full-32-Bit  Zero?   FUNCTION                  
BitOperationsLog2        2        Yes      Yes     BitOperations.Log2(x);
LeadingZeroCount         2        Yes      Yes     31 - BitOperations.LeadingZeroCount(x);
Log2_SunsetQuest5        16       Yes      No      ((BitConverter.DoubleToInt64Bits(val)>>52)+1) & 0xFF;
Log2_WiegleyJ            17       Yes      Yes     ...
MostSigBit_spender       17       Yes      Yes     ...
Log2_SPWorley            17       Yes      Yes     ...
Log2_SunsetQuest4        18       Yes      No      ...
FloorLg2_Matthew_Watson  18       Yes      Yes     ...
Log2_SunsetQuest3        19       Yes      No      ...
Log2_SunsetQuest1        20       Yes      Yes     ...
Log2_HarrySvensson       20       Yes      Yes     ...
Log2_DanielSig           21       No       Yes     ...
HighestBitUnrolled_Kaz   25       Yes      Yes     ...
FloorLog2_SN17           36       Yes      Yes     ...
Log2_Papayaved           44       Yes      Yes     ...
GetMsb_user3177100       45       Yes      Yes     ...
Log2_Flynn1179           57       Yes      Yes     ...
Msb_Protagonist          63       Yes      Yes     ...
SomeOtherMethod          76       Yes      Yes     ...
Log2_SunsetQuest0        98       Yes      Yes     ...
Log2_SunsetQuest2        131      Yes      Yes     ...
SomeOtherMethod          202      Yes      Yes     ...
SomeOtherMethod          545      Yes      Yes     ...

Zero_Support    = Supports Neg Return on Zero
Full-32-Bit     = Supports full 32-bit (some just support 31 bits)
SomeOtherMethod = name of function/person left out on purpose
Benchmark notes: AMD Ryzen, Release, no-debugger attached, .net 6.0

0
    static byte FloorLog2(UInt16 value)
    {
        for (byte i = 0; i < 15; ++i)
        {
            if ((value >>= 1) < 1)
            {
                return i;
            }
        }
        return 15;
    }

2
最好的做法是至少添加一个关于代码示例如何解决原始问题的小注释。 - camba1

0

我没有进行任何测量,所以可能不匹配,但我认为用户user9337139的想法很好,想在C#中尝试相同的方法-他的是C++。

这里是一个基于将字节值转换为浮点数并从IEEE浮点表示中提取指数的C# int Magnitude(byte)函数。

    using System.Runtime.InteropServices;

    [StructLayout(LayoutKind.Explicit)]
    struct UnionWorker
    {
        [FieldOffset(0)]
        public int i;
        [FieldOffset(0)]
        public float f;
    }

    static int Magnitude(byte b)
    {
        UnionWorker u;
        u.i = 0; // just to please the compiler
        u.f = b;
        return Math.Max((u.i >> 23) & 0xFF, 126) - 126;
    }

返回零对于零,0xFF 返回8,其他值按预期返回。

零是一个特殊情况,所以我需要使用 Math.Max 来限制它。我怀疑 user9337139 的解决方案可能会有类似的问题。

请注意,这个程序没有经过字节序问题的测试 - 买家自负。


1
我刚刚注意到你的回答,以为它是我的副本,但后来我看了一下日期。我在你之后仅仅几天发布了相同的答案(见GitHub)。这样接近的答案在同一天出现的概率有多大呢?无论如何,我只是想分享这个世间的奇妙巧合。 - SunsetQuest

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接