422

I was benchmarking some code, and I could not get it to run as fast as with java.math.BigInteger, even when using the exact same algorithm. So I copied java.math.BigInteger source into my own package and tried this:

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

When I run this (jdk 1.8.0_144-b01 on MacOS) it outputs:

12089nsec/mul
2559044166

When I run it with the import line uncommented:

4098nsec/mul
2559044166

It's almost three times as fast when using the JDK version of BigInteger versus my version, even if it's using the exact same code.

I've examined the bytecode with javap, and compared compiler output when running with options:

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

and both versions seem to generate the same code. So is hotspot using some precomputed optimisations that I can't use in my code? I always understood that they don't. What explains this difference?

Andronicus
  • 25,419
  • 17
  • 47
  • 88
Koen Hendrikx
  • 2,243
  • 2
  • 9
  • 7
  • 33
    Interesting. 1. Is the result consistent (or just lucky random )? 2. Can you try after warming JVM up ? 3. Can you eliminate random factor and provide same dataset as input for both the test ? – jmj Aug 28 '17 at 05:59
  • 8
    Did you try running your benchmark with JMH http://openjdk.java.net/projects/code-tools/jmh/ ? It is not that easy to make measurements correctly manually (warm up and all that stuff). – Roman Puchkovskiy Aug 28 '17 at 06:09
  • 2
    Yes it is very consistent. If I let it run for 10 minutes I still get the same difference. The fixed random seed ensures that both runs get the same dataset. – Koen Hendrikx Aug 28 '17 at 06:11
  • 6
    You probably still want JMH, just in case. And you should put up your modified BigInteger somewhere so people can reproduce your test and verify you're running what you think you're running. – pvg Aug 28 '17 at 06:14

2 Answers2

548

Yes, HotSpot JVM is kind of "cheating", because it has a special version of some BigInteger methods that you won't find in Java code. These methods are called JVM intrinsics.

In particular, BigInteger.multiplyToLen is an intrinsic method in HotSpot. There is a special hand-coded assembly implementation in JVM source base, but only for x86-64 architecture.

You may disable this intrinsic with -XX:-UseMultiplyToLenIntrinsic option to force JVM to use pure Java implementation. In this case the performance will be similar to the performance of your copied code.

P.S. Here is a list of other HotSpot intrinsic methods.

Holger
  • 285,553
  • 42
  • 434
  • 765
apangin
  • 92,924
  • 10
  • 193
  • 247
  • Why wouldn't the user's identical java code also have the same instrinsics substituted? The intrinsic is somehow only for a specific method in the JVM? – President James K. Polk Sep 22 '21 at 11:46
  • 2
    @PresidentJamesK.Polk Java code doesn't matter. It is used only in the interpreter. The whole point of intrinsics is to *ignore* Java code of the method in favor of the JVM built-in implementation. There is a hard-coded list of method names (with class names) that are subject to replacement. If you rename a class or a method, it will no longer be intrinsified. – apangin Sep 22 '21 at 12:45
  • Wow, I don't know how I didn't already know this. Looking at that list of methods I finally understand some things about Java performance, especially for the atomic methods. Thank you. – President James K. Polk Sep 23 '21 at 14:52
148

In Java 8 this is indeed an intrinsic method; a slightly modified version of the method:

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

Running this with:

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

This will print lots of lines and one of them will be:

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

In Java 9 on the other hand that method seems to not be an intrinsic anymore, but in turn it calls a method that is an intrinsic:

 @HotSpotIntrinsicCandidate
 private static int[] implMultiplyToLen

So running the same code under Java 9 (with the same parameters) will reveal:

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

Underneath it's the same code for the method - just a slightly different naming.

Hearen
  • 7,420
  • 4
  • 53
  • 63
Eugene
  • 117,005
  • 15
  • 201
  • 306