【问题标题】:How do you calculate log base 2 in Java for integers?如何在 Java 中计算以 2 为底的整数?
【发布时间】:2011-03-19 07:36:49
【问题描述】:

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

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

它是否具有最佳性能?

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

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

UPD2 由于cmets我会进行更详细的调查。

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

【问题讨论】:

  • 你是如何测试这个性能的?在我的系统(Core i7,jdk 1.6 x64)上,整数版本几乎比浮点版本快 10 倍。请务必对函数的结果进行实际操作,以免 JIT 完全删除计算!
  • 你是对的。我没有使用计算结果,编译器已经优化了一些东西。现在我的结果和你一样——整数函数快 10 倍(Core 2 Duo,jdk 1.6 c64)
  • 这实际上给了你Math.floor(Math.log(n)/Math.log(2)),所以它并不是真正计算以2为底的对数!

标签: java performance discrete-mathematics logarithm


【解决方案1】:

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

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 位客户端虚拟机:

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

JDK 1.7 x64 服务器虚拟机:

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

【讨论】:

  • x86 的BSR 指令执行32 - numberOfLeadingZeros,但未定义为0,因此(JIT)编译器必须检查非零,如果它不能证明它不需要。 BMI 指令集扩展(Haswell 和更新版本)引入了LZCNT,它在一条指令中完全实现了numberOfLeadingZeros。它们都是 3 个周期延迟,每个周期 1 个吞吐量。所以我绝对推荐使用numberOfLeadingZeros,因为这使得一个好的JVM变得容易。 (关于lzcnt 的一件奇怪的事情是它对它覆盖的寄存器的旧值有错误的依赖关系。)
  • 我最感兴趣的是您对 Java 1.7 服务器 JIT CPU 内在函数替换的评论。你有参考网址吗? (JIT源代码链接也可以。)
【解决方案2】:

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

我通常尽量避免 FP 计算。

浮点运算并不精确。您永远无法确定 (int)(Math.log(65536)/Math.log(2)) 的评估结果。例如,Math.ceil(Math.log(1&lt;&lt;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);
        }
    }
}

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

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。 你能在测试前告诉这个吗? 我绝对不能。

【讨论】:

  • “这并不意味着它可以在任何 PC 上以相同的方式工作”——如果你使用 strictfp 会这样,不是吗?
  • @Ken:也许...但是只有在详尽枚举所有可能的输入值之后才能确定。 (我们很幸运,这里的人很少)
  • 从技术上讲,是的,但任何函数都是如此。在某些时候,您必须相信,如果您使用可用的文档,并测试“所有可能的输入值”中的一些精心挑选但微不足道的一小部分,那么您的程序将运行得足够好。 strictfp 似乎实际上因为严格而受到了很多废话。 :-)
  • return ((long)Math.log(x) / (long)Math.log(base)); 解决所有错误怎么样?
  • @Notabug 对此不确定,但副作用之一是您的代码对于任何不适合 long 的值都将无法正常工作,如果您的值范围超过 long,这可能没有用范围(在 java 中 float 的范围比 long 高得多)
【解决方案3】:

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

【讨论】:

  • 虽然这在数学上是正确的,但请注意,由于浮点运算不精确,存在计算错误的风险,如 Rotsor 的回答中所述。
【解决方案4】:

你可以使用身份

            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

【讨论】:

  • 虽然这在数学上是正确的,但请注意,由于浮点运算不精确,存在计算错误的风险,如 Rotsor 的回答中所述。
【解决方案5】:

为什么不:

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

【讨论】:

  • 虽然这在数学上是正确的,但请注意,由于浮点运算不精确,存在计算错误的风险,如 Rotsor 的回答中所述。
【解决方案6】:

guava 库中有这个功能:

LongMath.log2()

所以我建议使用它。

【讨论】:

  • 如何将此包添加到我的应用程序中?
  • here 下载 jar 并将其添加到项目的构建路径中。
  • 我是否应该在我的应用程序中添加一个库只是为了使用一个功能?
  • 为什么你会建议使用它?对 Guava 源代码的快速阅读表明,它与 OP 的方法(一些非常清楚理解的代码行)做同样的事情,但代价是添加了一个原本无用的依赖项。仅仅因为 Google 提供了一些东西,并不比自己理解问题和解决方案更好。
【解决方案7】:

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

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

【讨论】:

    【解决方案8】:

    要添加到 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);
    }
    

    【讨论】:

    • “数字”变量在哪里?
    【解决方案9】:

    让我们补充一下:

    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 要求一种更快的方法来“计算”对数。
    【解决方案10】:

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

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

    【讨论】:

    • 这个答案已经发布了好几次,并且已经注意到由于舍入错误可能不准确。注意 OP 要求提供积分值;完全不清楚从这里到整数需要使用什么舍入精度。
    猜你喜欢
    • 1970-01-01
    • 2018-10-24
    • 2023-01-10
    • 2016-05-20
    • 1970-01-01
    • 2020-12-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多