如何用Java计算整数中的log base 2?

Nul*_*ice 134 java performance logarithm discrete-mathematics

我使用以下函数来计算整数的log base 2:

public static int log2(int n){
    if(n <= 0) throw new IllegalArgumentException();
    return 31 - Integer.numberOfLeadingZeros(n);
}
Run Code Online (Sandbox Code Playgroud)

它有最佳性能吗?

有人知道为此目的准备好J2SE API函数吗?

UPD1令 我惊讶的是,浮点算术似乎比整数算术更快.

UPD2 由于评论,我将进行更详细的调查.

UPD3 我的整数运算函数比Math.log(n)/Math.log(2)快10倍.

x4u*_*x4u 89

这是我用于此计算的函数:

public static int binlog( int bits ) // returns 0 for bits=0
{
    int log = 0;
    if( ( bits & 0xffff0000 ) != 0 ) { bits >>>= 16; log = 16; }
    if( bits >= 256 ) { bits >>>= 8; log += 8; }
    if( bits >= 16  ) { bits >>>= 4; log += 4; }
    if( bits >= 4   ) { bits >>>= 2; log += 2; }
    return log + ( bits >>> 1 );
}
Run Code Online (Sandbox Code Playgroud)

它比Integer.numberOfLeadingZeros()(20-30%)略快,比基于Math.log()的实现快几十倍(jdk 1.6 x64),如下所示:

private static final double log2div = 1.000000000001 / Math.log( 2 );
public static int log2fp0( int bits )
{
    if( bits == 0 )
        return 0; // or throw exception
    return (int) ( Math.log( bits & 0xffffffffL ) * log2div );
}
Run Code Online (Sandbox Code Playgroud)

两个函数都为所有可能的输入值返回相同的结果.

更新: Java 1.7服务器JIT能够使用基于CPU内在函数的替代实现替换一些静态数学函数.其中一个函数是Integer.numberOfLeadingZeros().因此,对于1.7或更新的服务器VM,问题中的实现实际上比binlog上面稍快.不幸的是,客户端JIT似乎没有这种优化.

public static int log2nlz( int bits )
{
    if( bits == 0 )
        return 0; // or throw exception
    return 31 - Integer.numberOfLeadingZeros( bits );
}
Run Code Online (Sandbox Code Playgroud)

对于所有2 ^ 32个可能的输入值,此实现也返回与上面发布的其他两个实现相同的结果.

以下是我电脑上的实际运行时间(Sandy Bridge i7):

JDK 1.7 32位客户端VM:

binlog:         11.5s
log2nlz:        16.5s
log2fp:        118.1s
log(x)/log(2): 165.0s
Run Code Online (Sandbox Code Playgroud)

JDK 1.7 x64服​​务器VM:

binlog:          5.8s
log2nlz:         5.1s
log2fp:         89.5s
log(x)/log(2): 108.1s
Run Code Online (Sandbox Code Playgroud)

这是测试代码:

int sum = 0, x = 0;
long time = System.nanoTime();
do sum += log2nlz( x ); while( ++x != 0 );
time = System.nanoTime() - time;
System.out.println( "time=" + time / 1000000L / 1000.0 + "s -> " + sum );
Run Code Online (Sandbox Code Playgroud)

  • x86的`BSR`指令执行`32 - numberOfLeadingZeros`,但未定义为0,因此(JIT)编译器必须检查非零,如果它不能证明它不必.BMI指令集扩展(Haswell和更新)引入了`LZCNT`,它在一条指令中完全实现了`numberOfLeadingZeros`.它们都是3个循环延迟,每个循环吞吐量1个.所以我绝对建议使用`numberOfLeadingZeros`,因为这样可以很容易地获得一个好的JVM.(关于`lzcnt`的一个奇怪的事情是它对它覆盖的寄存器的旧值有一个错误的依赖.) (8认同)

Rot*_*sor 71

如果您正在考虑使用浮点来帮助整数算术,那么您必须要小心.

我通常尽可能避免FP计算.

浮点运算并不准确.你永远无法确定将(int)(Math.log(65536)/Math.log(2))评估的内容.例如,Math.ceil(Math.log(1<<29) / Math.log(2))在我的PC上是30,在数学上它应该是29.我没有找到x的值(int)(Math.log(x)/Math.log(2))失败(因为只有32个"危险"值),但这并不意味着它将起作用在任何PC上都一样.

通常的技巧是在舍入时使用"epsilon".喜欢(int)(Math.log(x)/Math.log(2)+1e-10)永远不会失败.选择这个"epsilon"并不是一项微不足道的任务.

使用更一般的任务进行更多演示 - 尝试实现int log(int x, int base):

测试代码:

static int pow(int base, int power) {
    int result = 1;
    for (int i = 0; i < power; i++)
        result *= base;
    return result;
}

private static void test(int base, int pow) {
    int x = pow(base, pow);
    if (pow != log(x, base))
        System.out.println(String.format("error at %d^%d", base, pow));
    if(pow!=0 && (pow-1) != log(x-1, base))
        System.out.println(String.format("error at %d^%d-1", base, pow));
}

public static void main(String[] args) {
    for (int base = 2; base < 500; base++) {
        int maxPow = (int) (Math.log(Integer.MAX_VALUE) / Math.log(base));
        for (int pow = 0; pow <= maxPow; pow++) {
            test(base, pow);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果我们使用最直接的对数实现,

static int log(int x, int base)
{
    return (int) (Math.log(x) / Math.log(base));
}
Run Code Online (Sandbox Code Playgroud)

这打印:

error at 3^5
error at 3^10
error at 3^13
error at 3^15
error at 3^17
error at 9^5
error at 10^3
error at 10^6
error at 10^9
error at 11^7
error at 12^7
...
Run Code Online (Sandbox Code Playgroud)

为了完全消除错误,我必须添加介于1e-11和1e-14之间的epsilon.你能在测试之前告诉过这个吗?我绝对不能.

  • "这并不意味着它会在任何PC上以相同的方式工作" - 如果你使用`strictfp`,那会不会? (3认同)
  • 从技术上讲,是的,但是任何功能都是如此。在某些时候,您必须相信,如果您使用可用的文档,并测试“所有可能的输入值”中经过精心选择但几乎消失的一小部分,则您的程序将运行良好。实际上,`strictfp`似乎已经变得很严格了。:-) (2认同)

Chr*_* B. 34

尝试 Math.log(x) / Math.log(2)

  • 虽然在数学上这是正确的,但请注意,由于不精确的浮点运算,存在误计算的风险,如Rotsor的答案所述. (6认同)

hvg*_*des 27

你可以使用身份

            log[a]x
 log[b]x = ---------
            log[a]b
Run Code Online (Sandbox Code Playgroud)

所以这适用于log2.

            log[10]x
 log[2]x = ----------
            log[10]2
Run Code Online (Sandbox Code Playgroud)

只需将其插入java Math log10方法....

http://mathforum.org/library/drmath/view/55565.html

  • 虽然在数学上这是正确的,但请注意,由于不精确的浮点运算,存在误计算的风险,如Rotsor的答案所述. (2认同)

Tof*_*eer 18

为什么不:

public static double log2(int n)
{
    return (Math.log(n) / Math.log(2));
}
Run Code Online (Sandbox Code Playgroud)

  • 虽然在数学上这是正确的,但请注意,由于不精确的浮点运算,存在误计算的风险,如Rotsor的答案所述. (4认同)

小智 9

番石榴库中有功能:

LongMath.log2()
Run Code Online (Sandbox Code Playgroud)

所以我建议使用它.

  • 为什么你建议使用它?快速阅读Guava源代码表明,它与OP的方法(一些非常清楚的代码行)完全相同,但代价是添加了无用的依赖.仅仅因为谷歌提供的东西并没有比自己理解问题和解决方案更好. (5认同)
  • 我应该在我的应用程序中添加一个库才能使用一个函数吗? (2认同)

Mar*_*ina 7

当我使用 Math.log10 时,某些情况才有效:

public static double log2(int n)
{
    return (Math.log10(n) / Math.log10(2));
}
Run Code Online (Sandbox Code Playgroud)