如何计算Java中以整数为底的对数2?


138

我使用以下函数为整数计算对数基数2:

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

它是否具有最佳性能?

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

UPD1 对于我来说,令人惊讶的是,浮点运算似乎比整数运算要快。

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

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


1
您是如何测试其性能的?在我的系统(Core i7,jdk 1.6 x64)上,整数版本几乎比浮点版本快10倍。确保对函数的结果进行实际处理,以使JIT无法完全删除计算!
x4u

你是对的。我没有使用计算结果,并且编译器进行了优化。现在我得到与您相同的结果-整数函数快10倍(Core 2 Duo,jdk 1.6 c64)
Nulldevice

6
这有效地为您提供了服务Math.floor(Math.log(n)/Math.log(2)),因此它并不是真正计算日志基数2!
Dori

Answers:


74

如果您正在考虑使用浮点数来帮助进行整数运算,则必须小心。

我通常会尽量避免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)永远都不会失败。选择这种“ε”并不是一件容易的事。

使用更一般的任务进行更多的演示-尝试实现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);
        }
    }
}

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

static int log(int x, int base)
{
    return (int) (Math.log(x) / Math.log(base));
}

打印:

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
...

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


3
“这并不意味着它可以在任何PC上以相同的方式工作” –如果您使用的话strictfp,不是吗?

@肯:也许...但是您只能在穷举所有可能的输入值之后才能确定。(我们很幸运,这里的人很少)
Rotsor 2010年

2
从技术上讲,是的,但是任何功能都是如此。在某些时候,您必须相信,如果您使用可用的文档,并测试“所有可能的输入值”中一些精挑细选但几乎消失的部分,则您的程序将运行良好。 strictfp实际上,严格来说似乎似乎有很多废话。:-)

如何return ((long)Math.log(x) / (long)Math.log(base));解决所有错误?
不是错误

92

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

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 );
}

它比Integer.numberOfLeadingZeros()(20-30%)快一点,并且比基于Math.log()的实现快10倍(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 );
}

对于所有可能的输入值,两个函数均返回相同的结果。

更新: 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 );
}

该实现还针对所有2 ^ 32个可能的输入值返回了与我上面发布的其他两个实现相同的结果。

这是我的PC(Sandy Bridge i7)上的实际运行时:

JDK 1.7 32位客户端VM:

binlog:         11.5s
log2nlz:        16.5s
log2fp:        118.1s
log(x)/log(2): 165.0s

JDK 1.7 x64服​​务器VM:

binlog:          5.8s
log2nlz:         5.1s
log2fp:         89.5s
log(x)/log(2): 108.1s

这是测试代码:

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 );

9
x86的BSR指令具有32 - numberOfLeadingZeros,但未定义为0,因此(JIT)编译器必须检查非零值才能证明它不是必须的。引入了BMI指令集扩展(Haswell和更高版本)LZCNT,可以numberOfLeadingZeros在一条指令中完全完全实现。它们都是3个周期的延迟,每周期吞吐量1个。因此,我绝对建议您使用numberOfLeadingZeros,因为这样可以轻松实现良好的JVM。(一件奇怪的事情lzcnt是,它对它覆盖的寄存器的旧值有错误的依赖性。)
Peter Cordes

我对您对Java 1.7服务器JIT CPU内部函数替换的评论最感兴趣。您有参考网址吗?(JIT源代码链接也可以。)
kevinarpe

37

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


8
从数学上讲这是正确的,但请注意,由于不精确的浮点运算,存在计算错误的风险,如Rotsor的答案所述。
leeyuiwah

28

你可以使用身份

            log[a]x
 log[b]x = ---------
            log[a]b

因此这将适用于log2。

            log[10]x
 log[2]x = ----------
            log[10]2

只需将其插入Java Math log10方法即可。...

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


3
从数学上讲这是正确的,但请注意,由于不精确的浮点运算,存在计算错误的风险,如Rotsor的答案所述。
leeyuiwah

18

为什么不:

public static double log2(int n)
{
    return (Math.log(n) / Math.log(2));
}

6
从数学上讲这是正确的,但请注意,由于不精确的浮点运算,存在计算错误的风险,如Rotsor的答案所述。
leeyuiwah

9

番石榴库中有此功能:

LongMath.log2()

所以我建议使用它。


如何将该软件包添加到我的应用程序中?
Elvin Mammadov

这里下载jar 并将其添加到项目的构建路径。
Debosmit Ray

2
我是否应该在应用程序中添加一个库以仅使用一个功能?
Tash Pemhiwa '16

7
为什么您会建议使用它呢?快速阅读Guava的源代码可以发现它与OP的方法具有相同的功能(一些非常清楚地理解的代码行),但以增加原本无用的依赖为代价。仅仅因为Google提供了某些东西,并不能比您自己了解问题和解决方案更好。
戴夫

3

要添加到x4u答案中,该答案将为您提供数字二进制日志的下限,此函数返回数字二进制日志的ceil:

public static int ceilbinlog(int number) // returns 0 for bits=0
{
    int log = 0;
    int bits = number;
    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;
    }
    if (1 << log < number)
        log++;
    return log + (bits >>> 1);
}

“数字”变量在哪里?
barteks2x

3

当我使用Math.log10时,某些情况才起作用:

public static double log2(int n)
{
    return (Math.log10(n) / Math.log10(2));
}

0

让我们添加:

int[] fastLogs;

private void populateFastLogs(int length) {
    fastLogs = new int[length + 1];
    int counter = 0;
    int log = 0;
    int num = 1;
    fastLogs[0] = 0;
    for (int i = 1; i < fastLogs.length; i++) {
        counter++;
        fastLogs[i] = log;
        if (counter == num) {
            log++;
            num *= 2;
            counter = 0;
        }
    }
}

来源:https : //github.com/pochuan/cs166/blob/master/ps1/rmq/SparseTableRMQ.java


那将创建一个查找表。OP要求一种更快的方法来“计算”对数。
戴夫

-4

要计算n的对数以2为底,可以使用以下表达式:

double res = log10(n)/log10(2);

2
该答案已经发布了好几次,并且由于四舍五入错误而被注意到可能不正确。注意OP要求的积分值;尚不清楚需要使用哪种舍入精度才能从此处取整为整数。
AnotherParker '18
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.