Crossing a hedgehog with a hedgehog: OpenJDK-11 + GraalVM

Hello, Habr! In the light of not the most recent news about Oracle's policy regarding Java licensing, the issue of moving away from Oracle versions to OpenJDK is becoming more and more acute . Odanko at OracleLabs has long been doing a very cool thing called GraalVM , which is a cool JIT compiler written in Java, as well as a runtime for running code in languages ​​such as JavaScript, Ruby, Python, C, C ++, Scala, Kotlin, R, Clojure. Impressive, right? But not about the coolness of the polyglot environment, I want to tell you. We will talk about the difficulties of combining the latest grail assembly into the OpenJDK 11 ecosystem, and a bit about performance, quite a bit ...

First was the word


The story of my acquaintance with graalvm began on the joker in 2017. There Chris Seaton spoke in great detail about the compiler internals and showed the magic of AOT compilation using the native-image from the grail delivery as an example (this is such a joke that compiles your Java code into a native binary).

After that report, I trained for a very long time on compiling the native binary of my pet project, put crutches and rakes in order to earn reflection in all places (be it not okay!) And finally stumbled on unresolved problems with IO (something there it didn’t take off with a zookeeper, now I don’t remember what). Spit while on native-image :-(

Year 2018, all the same joker and the same graalvm in a super-detailed report from Oleg Shelaev about AOT.

In reports and presentations, everything looks so cool, and there’s also a pet project lying on the disc ... it's time to uncover the terminal, swing a fresh grail release candidate and go to battle! We will touch the jit.

Hello world


Touching and kicking a fresh little JIT (at the time of writing this article is the version ce-1.0.0-rc14 ) we will use the example of a piece of code for testing performance from the site https://graalvm.org - our first example.

What java project (even Hello World ) is doing without any build system? That's right, only the one that javac is learning to cook on. We won’t study Javak, let maven steer Javak.

So meet pom.xml:

pom.xml
4.0.0com.mycompany.appmy-app
        jar-->jar1.0-SNAPSHOTmy-apphttp://maven.apache.org111.0.0-rc14jdk1111org.apache.maven.pluginsmaven-dependency-plugin2.10copyprocess-test-classescopy-dependencies${project.build.directory}/liborg.apache.maven.pluginsmaven-jar-plugincom.mycompany.app.Appmaven-compiler-plugin3.8.0${java.version}junitjunit4.12testorg.graalvm.compilercompiler${graalvm.version}org.graalvm.truffletruffle-api${graalvm.version}org.graalvm.sdkgraal-sdk${graalvm.version}org.graalvm.jsjs${graalvm.version}org.graalvm.jsjs-scriptengine${graalvm.version}


The project file structure looks like this:



Class code com.mycompany.app.App (copy-paste of the example from graalvm.org):

App.java
    package com.mycompany.app;
    public class App {
        static final int ITERATIONS = Math.max(Integer.getInteger("iterations", 1), 1);
        public static void main(String[] args) {
            String sentence = String.join(" ", args);
            for (int iter = 0; iter < ITERATIONS; iter++) {
                if (ITERATIONS != 1) System.out.println("-- iteration " + (iter + 1) + " --");
                long total = 0, start = System.currentTimeMillis(), last = start;
                for (int i = 1; i < 10_000_000; i++) {
                    total += sentence.chars().filter(Character::isUpperCase).count();
                    if (i % 1_000_000 == 0) {
                        long now = System.currentTimeMillis();
                        System.out.printf("%d (%d ms)%n", i / 1_000_000, now - last);
                        last = now;
                    }
                }
                System.out.printf("total: %d (%d ms)%n", total, System.currentTimeMillis() - start);
            }
        }
    }
    


Code module-info.java :

module-info.java
    module com.mycompany.app {}
    


Hmm, empty ... And it's empty for the reason that I want to show you how custom java (about custom java a bit later) will swear with undeclared modules that our application needs.

The first pancake ...


not lumpy. Let's assemble our project and launch it. Putting it this way:

mvn clean package


We launch:


$JAVA_HOME/bin/java -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler -Diterations=10 -jar target/my-app-1.0-SNAPSHOT.jar In 2017 I would like to run ALL languages in one VM.
    

There are two important points here: JVMCI is an experimental thing that has appeared in Java since version 9, so we need:

  • enable experimental virtual machine options -
    -XX: + UnlockEperimentalVMOptions
  • enable the very experimental virtual machine (starting from nine the grail is in openjdk, not the latest version, but still) - -XX: + UseJVMCICompiler

Launch result
- iteration 1 - 1 (1466 ms)
2 (461 ms)
3 (463 ms)
4 (138 ms)
5 (151 ms)
6 (159 ms)
7 (266 ms)
8 (128 ms)
9 (144 ms)
total : 69999993 (3481 ms)
- iteration 2 - 1 (233 ms)
2 (169 ms)
3 (121 ms)
4 (205 ms)
5 (170 ms)
6 (152 ms)
7 (227 ms)
8 (158 ms)
9 (108 ms)
total: 69999993 (1644 ms)
- iteration 3 - 1 (98 ms)
2 (102 ms)
3 (98 ms)
4 (102 ms)
5 (95 ms)
6 (96 ms)
7 (101 ms )
8 (95 ms)
9 (97 ms)
total: 69999993 (990 ms)
- iteration 4 - 1 (109 ms)
2 (114 ms)
3 (97 ms)
4 (98 ms)
5 (100 ms)
6 (103 ms)
7 (125 ms)
8 (108 ms)
9 (100 ms)
total : 69999993 (1056 ms)
- iteration 5 - 1 (98 ms)
2 (100 ms)
3 (105 ms)
4 (97 ms)
5 (95 ms)
6 (99 ms)
7 (95 ms)
8 (123 ms)
9 (98 ms)
total: 69999993 (1010 ms)
- iteration 6 - 1 (99 ms)
2 (95 ms)
3 (102 ms)
4 (99 ms)
5 (96 ms)
6 (100 ms)
7 (99 ms) )
8 (99 ms)
9 (104 ms)
total: 69999993 (993 ms)
- iteration 7 - 1 (100 ms)
2 (104 ms)
3 (95 ms)
4 (96 ms)
5 (97 ms)
6 (95 ms)
7 (94 ms)
8 (108 ms)
9 (108 ms)
total : 69999993 (1000 ms)
- iteration 8 - 1 (100 ms)
2 (106 ms)
3 (99 ms)
4 (95 ms)
5 (97 ms)
6 (97 ms)
7 (101 ms)
8 (99 ms)
9 (101 ms)
total: 69999993 (1012 ms)
- iteration 9 - 1 (105 ms)
2 (97 ms)
3 (98 ms)
4 (96 ms)
5 (99 ms)
6 (96 ms)
7 (94 ms) )
8 (98 ms)
9 (105 ms)
total: 69999993 (993 ms)
- iteration 10 - 1 (107 ms)
2 (98 ms)
3 (99 ms)
4 (100 ms)
5 (97 ms)
6 (101 ms)
7 (98 ms)
8 (103 ms)
9 (105 ms)
total : 69999993 (1006 ms)

What do we see here? And we see that the first iteration is the longest (3.5 seconds), this JIT is warming up. And then everything is more or less smooth (within one second).

What if we give Java a fresh version of the grail? No sooner said than done:


    java -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler -Diterations=10 --module-path=target/lib --upgrade-module-path=target/lib/compiler-1.0.0-rc14.jar -jar target/my-app-1.0-SNAPSHOT.jar In 2017 I would like to run ALL languages in one VM.
    

Fresh grail launch result
- iteration 1 - 1 (1789 ms)
2 (547 ms)
3 (313 ms)
4 (87 ms)
5 (88 ms)
6 (87 ms)
7 (83 ms)
8 (92 ms)
9 (87 ms)
total : 69999993 (3259 ms)
- iteration 2 - 1 (241 ms)
2 (161 ms)
3 (152 ms)
4 (195 ms)
5 (136 ms)
6 (129 ms)
7 (154 ms)
8 (176 ms)
9 (109 ms)
total: 69999993 (1553 ms)
- iteration 3 - 1 (109 ms)
2 (103 ms)
3 (113 ms)
4 (172 ms)
5 (141 ms)
6 (148 ms)
7 (111 ms )
8 (102 ms)
9 (101 ms)
total: 69999993 (1211 ms)
- iteration 4 - 1 (96 ms)
2 (96 ms)
3 (104 ms)
4 (98 ms)
5 (96 ms)
6 (97 ms)
7 (98 ms)
8 (96 ms)
9 (95 ms)
total : 69999993 (972 ms)
- iteration 5 - 1 (97 ms)
2 (93 ms)
3 (99 ms)
4 (97 ms)
5 (97 ms)
6 (97 ms)
7 (95 ms)
8 (98 ms)
9 (94 ms)
total: 69999993 (965 ms)
- iteration 6 - 1 (96 ms)
2 (95 ms)
3 (96 ms)
4 (99 ms)
5 (102 ms)
6 (94 ms)
7 (99 ms) )
8 (115 ms)
9 (109 ms)
total: 69999993 (1001 ms)
- iteration 7 - 1 (98 ms)
2 (96 ms)
3 (99 ms)
4 (98 ms)
5 (118 ms)
6 (98 ms)
7 (95 ms)
8 (99 ms)
9 (116 ms)
total : 69999993 (1017 ms)
- iteration 8 - 1 (95 ms)
2 (99 ms)
3 (99 ms)
4 (106 ms)
5 (101 ms)
6 (101 ms)
7 (93 ms)
8 (97 ms)
9 (108 ms)
total: 69999993 (993 ms)
- iteration 9 - 1 (102 ms)
2 (95 ms)
3 (97 ms)
4 (125 ms)
5 (94 ms)
6 (101 ms)
7 (100 ms )
8 (95 ms)
9 (96 ms)
total: 69999993 (1008 ms)
- iteration 10 - 1 (97 ms)
2 (97 ms)
3 (99 ms)
4 (112 ms)
5 (102 ms)
6 (96 ms)
7 (96 ms)
8 (98 ms)
9 (96 ms)
total : 69999993 (988 ms)

The result, as we see, is not much different.

I forgot. We didn’t try to run the same thing without the new-fangled JIT compiler. We will do:


    java -Diterations=10 -jar target/my-app-1.0-SNAPSHOT.jar In 2017 I would like to run ALL languages in one VM.
    

Result without a newfangled JIT compiler
- iteration 1 - 1 (372 ms)
2 (271 ms)
3 (337 ms)
4 (391 ms)
5 (328 ms)
6 (273 ms)
7 (239 ms)
8 (271 ms)
9 (250 ms)
total : 69999993 (2978 ms)
- iteration 2 - 1 (242 ms)
2 (253 ms)
3 (253 ms)
4 (240 ms)
5 (245 ms)
6 (275 ms)
7 (273 ms)
8 (263 ms)
9 (234 ms)
total: 69999993 (2533 ms)
- iteration 3 - 1 (237 ms)
2 (235 ms)
3 (234 ms)
4 (246 ms)
5 (242 ms)
6 (238 ms)
7 (244 ms )
8 (243 ms)
9 (253 ms)
total: 69999993 (2414 ms)
- iteration 4 - 1 (244 ms)
2 (249 ms)
3 (245 ms)
4 (243 ms)
5 (232 ms)
6 (256 ms)
7 (321 ms)
8 (303 ms)
9 (249 ms)
total : 69999993 (2599 ms)
- iteration 5 - 1 (246 ms)
2 (242 ms)
3 (248 ms)
4 (256 ms)
5 (280 ms)
6 (233 ms)
7 (235 ms)
8 (266 ms)
9 (246 ms)
total: 69999993 (2511 ms)
- iteration 6 - 1 (292 ms)
2 (368 ms)
3 (339 ms)
4 (251 ms)
5 (267 ms)
6 (259 ms)
7 (289 ms )
8 (262 ms)
9 (357 ms)
total: 69999993 (3058 ms)
- iteration 7 - 1 (284 ms)
2 (258 ms)
3 (248 ms)
4 (247 ms)
5 (266 ms)
6 (247 ms)
7 (242 ms)
8 (314 ms)
9 (265 ms)
total : 69999993 (2656 ms)
- iteration 8 - 1 (239 ms)
2 (238 ms)
3 (257 ms)
4 (282 ms)
5 (244 ms)
6 (261 ms)
7 (253 ms)
8 (295 ms)
9 (256 ms)
total: 69999993 (2575 ms)
- iteration 9 - 1 (273 ms)
2 (243 ms)
3 (239 ms)
4 (240 ms)
5 (250 ms)
6 (285 ms)
7 (266 ms) )
8 (285 ms)
9 (264 ms)
total: 69999993 (2617 ms)
- iteration 10 - 1 (245 ms)
2 (264 ms)
3 (258 ms)
4 (253 ms)
5 (239 ms)
6 (260 ms)
7 (251 ms)
8 (250 ms)
9 (256 ms)
total : 69999993 (2538 ms)

The result is different, and decent.

C2 does not provide any optimizations on hot pieces of code - each iteration with the same time.

Graal is able to optimize hot pieces of code and in the long run gives a good performance boost.

For what?


This is perhaps the main question that you need to ask yourself and others (team members) when we want to add a new feature to the project, a new tool, a new virtual machine, a new JIT ...

My story, as described above, began with Joker 2017, then there were long attempts to overpower AOT, and now I taste the delights of JIT for Java in Java.

A pet project on disk is a kind of business process engine, where processes are drawn by application developers in the browser UI, and they have the ability to write scripts in JS that will run on the JVM.

In future versions of Java they promise to remove nashorn, GraalVM is gradually approaching the release ...

Well, the answer to the question is:

  1. we want a runtime to run JS (and not only)
  2. want speed jit
  3. we want the launch of pet-project applications to appear as before on 8-ke (without any
    --module-path
    and
    --upgrade-module-path
    but with a fresh grail assembly)

Jlink


The first two points in the list of answers to the above question are clear enough, let's deal with the latter.

The fact is that developers-admins-devops are lazy and don’t like to do extra work (I’m like that too), they try to automate everything and pack it into a ready-made bundle that can be run as a simple binar. Well, there is a problem, let's solve it.

A relatively new tool from the world of Java 9+ comes to our aid, and its name is jlink . We are trying to pack our application with all the necessary libs in a bundle:


jlink --module-path target/classes:target/lib:$JAVA_HOME/jmods --add-modules com.mycompany.app --launcher app=com.mycompany.app/com.mycompany.app.App --compress 2 --no-header-files --no-man-pages --strip-debug --output test
    

How many parameters of all kinds, we describe the main ones:
  • --module-path target / classes: target / lib: $ JAVA_HOME / jmods
  • --add-modules com.mycompany.app
  • --launcher app = com.mycompany.app / com.mycompany.app.App
  • --output test

You can ask Google’s uncle about the rest of the parameters; all of them are aimed at reducing the total size of the bundle.

Let's look at the result:



Inside test / bin / app there is a simple sh-script that launches our application on the Java that lies next to the app:


        #!/bin/sh
JLINK_VM_OPTIONS="-Diterations=10" #системный параметр мы уже ручками задефайнили, изначально переменная тут с пустым значением
DIR=`dirname $0`
$DIR/java $JLINK_VM_OPTIONS -m com.mycompany.app/com.mycompany.app.App $@
    

Run test / bin / app on C2:


    ./test/bin/app In 2017 I would like to run ALL languages in one VM.
    

Result
- iteration 1 - 1 (315 ms)
2 (231 ms)
3 (214 ms)
4 (297 ms)
5 (257 ms)
6 (211 ms)
7 (217 ms)
8 (245 ms)
9 (222 ms)
total : 69999993 (2424 ms)
- iteration 2 - 1 (215 ms)
2 (215 ms)
3 (223 ms)
4 (224 ms)
5 (217 ms)
6 (208 ms)
7 (208 ms)
8 (222 ms)
9 (222 ms)
total: 69999993 (2164 ms)
- iteration 3 - 1 (206 ms)
2 (226 ms)
3 (234 ms)
4 (211 ms)
5 (212 ms)
6 (213 ms)
7 (210 ms )
8 (245 ms)
9 (223 ms)
total: 69999993 (2216 ms)
- iteration 4 - 1 (222 ms)
2 (233 ms)
3 (220 ms)
4 (222 ms)
5 (221 ms)
6 (219 ms)
7 (222 ms)
8 (216 ms)
9 (220 ms)
total : 69999993 (2215 ms)
- iteration 5 - 1 (231 ms)
2 (230 ms)
3 (221 ms)
4 (226 ms)
5 (227 ms)
6 (223 ms)
7 (215 ms)
8 (216 ms)
9 (219 ms)
total: 69999993 (2234 ms)
- iteration 6 - 1 (227 ms)
2 (218 ms)
3 (221 ms)
4 (254 ms)
5 (222 ms)
6 (212 ms)
7 (214 ms )
8 (222 ms)
9 (222 ms)
total: 69999993 (2241 ms)
- iteration 7 - 1 (217 ms)
2 (225 ms)
3 (222 ms)
4 (223 ms)
5 (227 ms)
6 (221 ms)
7 (219 ms)
8 (226 ms)
9 (219 ms)
total : 69999993 (2217 ms)
- iteration 8 - 1 (218 ms)
2 (242 ms)
3 (219 ms)
4 (218 ms)
5 (224 ms)
6 (226 ms)
7 (223 ms)
8 (220 ms)
9 (219 ms)
total: 69999993 (2228 ms)
- iteration 9 - 1 (234 ms)
2 (218 ms)
3 (217 ms)
4 (217 ms)
5 (225 ms)
6 (222 ms)
7 (216 ms) )
8 (226 ms)
9 (214 ms)
total: 69999993 (2212 ms)
- iteration 10 - 1 (226 ms)
2 (230 ms)
3 (215 ms)
4 (238 ms)
5 (225 ms)
6 (218 ms)
7 (218 ms)
8 (215 ms)
9 (228 ms)
total : 69999993 (2233 ms)

Now on graalvm (by defining the necessary flags to run in the JLINK_VM_OPTIONS variable ):

test / bin / app

 #!/bin/sh
JLINK_VM_OPTIONS="-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler -Diterations=10"
DIR=`dirname $0`
$DIR/java $JLINK_VM_OPTIONS -m com.mycompany.app/com.mycompany.app.App $@
        


Result:


Error occurred during initialization of boot layer
java.lang.module.FindException: Module jdk.internal.vm.ci not found
    

Well, we arrived ... And now remember that we are working with java 11 in a modular environment, we are building the application as a module, but they did not tell anyone about the modules used. It's time to fix it.

New version of module-info.java:

module com.mycompany.app {
    requires jdk.internal.vm.compiler;
    requires org.graalvm.sdk;
    requires org.graalvm.truffle;
    requires transitive org.graalvm.js;
    requires transitive org.graalvm.js.scriptengine;
}
    

We collect , delete the test directory, link .

Result:


Error: automatic module cannot be used with jlink: icu4j from file:///home/slava/JavaProjects/graal-js-jdk11-maven-demo/target/lib/icu4j-62.1.jar
    

What kind of “automatic module kennote bi uzd”? And this jlink tells us that the icu4j lib does not contain module-info.class. What is needed for such a class to appear inside the specified lib:

  • understand the list of modules used by any and create module-info.java , define all the packages that should be visible from the outside
  • compile module-info.java for
  • put the compiled module-info.java into the dzharnik with any

Go!

The module-info.java file with all its contents will be generated for us by the jdeps utility from openjdk-11:



Compile module-info.java for icu4j:



Update the dzharnik by pushing module-info.class into it :


        $JAVA_HOME/bin/jar uf target/lib/icu4j-62.1.jar -C target/modules module-info.class
    

Link , run .

Result
— iteration 1 — 1 (1216 ms)
2 (223 ms)
3 (394 ms)
4 (138 ms)
5 (116 ms)
6 (102 ms)
7 (120 ms)
8 (106 ms)
9 (110 ms)
total: 69999993 (2619 ms)
— iteration 2 — 1 (166 ms)
2 (133 ms)
3 (142 ms)
4 (157 ms)
5 (119 ms)
6 (134 ms)
7 (153 ms)
8 (95 ms)
9 (85 ms)
total: 69999993 (1269 ms)
— iteration 3 — 1 (86 ms)
2 (81 ms)
3 (87 ms)
4 (83 ms)
5 (85 ms)
6 (100 ms)
7 (87 ms)
8 (83 ms)
9 (85 ms)
total: 69999993 (887 ms)
— iteration 4 — 1 (84 ms)
2 (86 ms)
3 (88 ms)
4 (91 ms)
5 (85 ms)
6 (88 ms)
7 (87 ms)
8 (85 ms)
9 (85 ms)
total: 69999993 (864 ms)
— iteration 5 — 1 (94 ms)
2 (86 ms)
3 (84 ms)
4 (83 ms)
5 (85 ms)
6 (86 ms)
7 (84 ms)
8 (84 ms)
9 (83 ms)
total: 69999993 (854 ms)
— iteration 6 — 1 (83 ms)
2 (89 ms)
3 (87 ms)
4 (87 ms)
5 (86 ms)
6 (86 ms)
7 (91 ms)
8 (86 ms)
9 (85 ms)
total: 69999993 (865 ms)
— iteration 7 — 1 (87 ms)
2 (86 ms)
3 (88 ms)
4 (90 ms)
5 (91 ms)
6 (87 ms)
7 (85 ms)
8 (85 ms)
9 (86 ms)
total: 69999993 (868 ms)
— iteration 8 — 1 (84 ms)
2 (85 ms)
3 (86 ms)
4 (84 ms)
5 (84 ms)
6 (88 ms)
7 (85 ms)
8 (86 ms)
9 (86 ms)
total: 69999993 (852 ms)
— iteration 9 — 1 (83 ms)
2 (85 ms)
3 (84 ms)
4 (85 ms)
5 (89 ms)
6 (85 ms)
7 (88 ms)
8 (86 ms)
9 (83 ms)
total: 69999993 (850 ms)
— iteration 10 — 1 (83 ms)
2 (84 ms)
3 (83 ms)
4 (82 ms)
5 (85 ms)
6 (83 ms)
7 (84 ms)
8 (94 ms)
9 (93 ms)
total: 69999993 (856 ms)

HURRAH! We made it! Now we have a banned application in the form of a running sh-script with our Java, with all the necessary modules (including fresh graalvm), with preference and young ladies.

PS


Java does not get bored and gives new food for the mind with each release. Try new features, experiment, share experience. I hope I’ll write an article soon about how I banned part of the pet project with the grail (there vert.x, asynchronism and js-scripts will be interesting).

And yet ... this is my first article on Habré, - please do not hit hard.

Also popular now: