Java JIT在运行JDK代码时是否作弊?

423

我正在对一些代码进行基准测试,但即使使用完全相同的算法,我也无法像使用java.math.BigInteger那样运行得快。因此,我将java.math.BigInteger源代码复制到我的程序包中,并尝试了以下操作:

//import java.math.BigInteger;

public class MultiplyTest {
    public static void main(String[] args) {
        Random r = new Random(1);
        long tm = 0, count = 0,result=0;
        for (int i = 0; i < 400000; i++) {
            int s1 = 400, s2 = 400;
            BigInteger a = new BigInteger(s1 * 8, r), b = new BigInteger(s2 * 8, r);
            long tm1 = System.nanoTime();
            BigInteger c = a.multiply(b);
            if (i > 100000) {
                tm += System.nanoTime() - tm1;
                count++;
            }
            result+=c.bitLength();
        }
        System.out.println((tm / count) + "nsec/mul");
        System.out.println(result); 
    }
}
当我在MacOS上运行此代码(jdk 1.8.0_144-b01),它会输出:
12089nsec/mul
2559044166

当我取消注释import语句并运行它时:


4098nsec/mul
2559044166

如果使用JDK版本的BigInteger,即使使用完全相同的代码,速度也比使用我的版本快近三倍。

我已经使用javap检查了字节码,并比较了在使用选项运行时的编译器输出。

-Xbatch -XX:-TieredCompilation -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions 
-XX:+PrintInlining -XX:CICompilerCount=1

两个版本似乎都生成了相同的代码。那么,热点是否使用了我不能在我的代码中使用的某些预计算优化?我一直以为它们不会这样做。是什么解释了这种差异呢?


33
有趣。
  1. 结果是否一致(或仅仅是幸运的随机结果)?
  2. 您能否在JVM预热后再试一次?
  3. 您能否消除随机因素并为测试提供相同的数据集作为输入?
- jmj
8
你是否尝试使用JMH(http://openjdk.java.net/projects/code-tools/jmh/)来运行你的基准测试?手动正确地进行测量并不容易(需要预热等等)。 - Roman Puchkovskiy
2
是的,它非常一致。如果我让它运行10分钟,仍然会得到相同的差异。固定的随机种子确保两次运行获得相同的数据集。 - Koen Hendrikx
6
您可能仍然需要 JMH,以防万一。您应该将修改后的 BigInteger 放在某个地方,以便其他人可以重现您的测试并验证您运行的代码与您认为的相同。 - pvg
2个回答

549

HotSpot JVM“作弊”,因为它有一些特殊版本的BigInteger方法,你在Java代码中找不到这些方法。这些方法称为JVM intrinsics

特别地,BigInteger.multiplyToLen是HotSpot中的内部方法。在JVM源代码库中有一个特殊的手写汇编实现,但仅适用于x86-64架构。

您可以使用-XX:-UseMultiplyToLenIntrinsic选项禁用此内部方法,以强制JVM使用纯Java实现。在这种情况下,性能将类似于复制代码的性能。

P.S.这里是其他HotSpot内置方法的列表


为什么用户相同的Java代码没有被替换为相同的内置函数?这个内置函数只适用于JVM中的特定方法吗? - President James K. Polk
2
@PresidentJamesK.Polk Java代码并不重要,它仅用于解释器。内置函数的整个重点是忽略方法中的Java代码,而是使用JVM内置实现。有一个硬编码的方法名称列表(带有类名),这些方法将被替换。如果您重命名类或方法,则不再会进行内部化处理。 - apangin
哇,我不知道为什么我之前不知道这个。看到那些方法列表,我终于理解了一些关于Java性能的东西,特别是原子方法。谢谢。 - President James K. Polk
我认为值得注意的是,根据你提供的链接,内置函数的数量相对较少。它们主要用于Class、数学、低级位操作和一些密码学相关的功能。 - undefined

148

Java 8 中,这确实是一种内置方法;这是该方法的稍微修改后的版本:

 private static BigInteger test() {

    Random r = new Random(1);
    BigInteger c = null;
    for (int i = 0; i < 400000; i++) {
        int s1 = 400, s2 = 400;
        BigInteger a = new BigInteger(s1 * 8, r), b = new BigInteger(s2 * 8, r);
        c = a.multiply(b);
    }
    return c;
}

使用以下方式运行:

 java -XX:+UnlockDiagnosticVMOptions  
      -XX:+PrintInlining 
      -XX:+PrintIntrinsics 
      -XX:CICompilerCount=2 
      -XX:+PrintCompilation   
       <YourClassName>

这将打印出许多行文字,其中之一是:

 java.math.BigInteger::multiplyToLen (216 bytes)   (intrinsic)

然而,在Java 9中,该方法似乎不再是内置的,相反它调用了一个内置的方法:

 @HotSpotIntrinsicCandidate
 private static int[] implMultiplyToLen

因此,在Java 9下运行相同的代码(使用相同的参数)将显示:

java.math.BigInteger::implMultiplyToLen (216 bytes)   (intrinsic)

在方法下面,其实是相同的代码 - 只是命名略有不同。


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