Skip to content

Latest commit

 

History

History
1301 lines (977 loc) · 53.9 KB

javaspektrum-graal-polyglot.adoc

File metadata and controls

1301 lines (977 loc) · 53.9 KB

Café Melange - Polyglot Operations in Java mit GraalVM

Wie auch beim Kotlin-Artikel, musste ich auch anderswo feststellen, dass ich meiner Zeit etwas voraus bin. Mein erster Artikel zu Truffle und Graal erschien in der JavaSpektrum 06/14, also vor 4 Jahren.

In dieser Zeit hat sich viel getan und wir alle können jetzt von den Ergebnissen des Projekts profitieren.

Graal und Truffle sind eine Entwicklung von OracleLabs, wo under der Leitung von Thomas Wuerthinger ein ansehnliches Team am Compiler, Sprachwerkzeugen, Unterstützung für dynamische Sprachen und Ahead-of-Time Kompilierung (AOT) arbeiten.

Seit Java 11 ist der Graal-Compiler ein offizieller Bestandteil des JDK und kann einfach mittels Kommandozeilenoptionen aktiviert werden. Die GraalVM ist in einer freien Community- und einer kommerziellen Enterprise-Edition zum Download verfügbar, sie soll die effiziente Unterstützung vieler Sprachen auf vielen Plattformen (OpenJDK, Nativ, Node, MySQL, Oracle) erlauben. Alle Sprachen und Entwicklertools sind in der Community-Edition nutzbar. Die Enterprise Edition hat zusätzliche Features und ca. 20% höhere Leistung, es gibt eine kostenlose Trial-Version. Aktuell ist gerade die Version 1.0-RC9, im nächsten Jahr soll die finale 1.0 Version erscheinen.

graalvm architecture
Figure 1. GraalVM Sprachen und Umgebungen - Quelle GraalVM Docs

Wie bereits früher ausgeführt ist die Hotspot-JVM eine Laufzeitumgebung, die in C/C++ implementiert ist. Neben anderer Infrastruktur, wie Speicher- und Netzwerkmanagement und Garbage-Collector enthält sie im Kern einen Interpreter und einen Just-in-Time (JIT) Compiler (zur Zeit C2), die Java Bytecode in Prozessorinstruktionen umsetzen.

Der JIT wird, genutzt um häufig ausgeführten Code (HotSpots) zu optimieren. Zum Thema Laufzeitoptimierung gab es von Cliff Click einen sehr interessanten Podcast [Click].

C2 ist ein sehr komplexes Stück Software dessen Weiterentwicklung und Wartung nicht trivial ist. Das ist auch einer der Gründe warum so selten neue Bytecode-Instruktionen in der JVM Einzug halten und Projekte wie Panama (Vektorisierung) nur langsam vorankommen. Daher ist der Plan den C2 abzulösen, ein Schritt auf dem Weg ist die Möglichkeit den Compiler der JVM austauschbar (mittels JVM Compiler Interface - JMVCI) zu machen.

Graal ist ein neuer Compiler, der selbst komplett in Java geschrieben ist und ein sauberes Design hat. Dabei nutzt er viele moderne Compileransätze zur Optimierung, z.B. Allokationvermeidung wenn festgestellt wird, dass Objekte den aktuellen Scope nicht verlassen. Dann werden nur die Felder des Objekts als primitive Werte auf den Stack oder in Register gelegt. Desweiteren ist Graal ein optimierender Compiler, d.h. aller Code der vom Programm nicht erreichbar ist, wird aggressiv entfernt (dead-code-elimination). Im steady-State macht Graal Annahmen über Datentypen, die, solange sie zutreffen, es ermöglichen sehr effiziente Prozessorbefehle einzusetzen. Das Graal Team sieht es als "Bug" an, wenn man keine Leistungsverbesserung mit dem neuen Compiler sieht.

Sprachvielfalt auf der JVM

Das Hauptthema dieses Artikels, ist die Fähigkeit von Graal, viele dynamische Sprachen effizient in der JVM auszuführen und auch dedizierte Runtimes für diese Sprachen bereitzustellen, die schneller, flexibler und sicherer sind als die originalen Interpreter/Compiler der jeweiligen Sprache.

Installation

Der Graal Compiler kann in OpenJDK 11 mittels -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler aktiviert werden. Ich habe bequemerweise [SDKMAN] genutzt, um die GraalVM zu installieren. Die Sprachen, die zur Zeit vom Graal-Team offiziell unterstützt und dokumentiert werden sind: JavaScript, Ruby, R, Python (neu) und LLVM (u.a. aus C/C++/Swift/Rust) Bitcode.

Beim Download der Graal-VM ist JavaScript und LLVM Unterstützung schon enthalten, auch die notwendigen Bibliotheken sind schon im Klassenpfad vorhanden.

Die Bibliotheken und Laufzeitumgebungen für Ruby, Python, R können über das Kommandozeilentool gu (Graal Updater) installiert werden.

Installation GraalVM mit sdkman und Zusatzsprachen mit gu
sdk use java 1.0.0-rc-11-grl
gu install ruby python R

$JAVA_HOME/jre/bin/java -version
openjdk version "1.8.0_192"
OpenJDK Runtime Environment (build 1.8.0_192-20181024123616.buildslave.jdk8u-src-tar--b12)
GraalVM 1.0.0-rc9 (build 25.192-b12-jvmci-0.49, mixed mode)

$JAVA_HOME/jre/bin/graalpython --version
Graal Python 3.7.0 (GraalVM CE Native 1.0.0-rc9)

$JAVA_HOME/jre/bin/Rscript --version
R scripting front-end version 3.4.0 (2017-04-21)

$JAVA_HOME/jre/bin/ruby --version
truffleruby 1.0.0-rc9, like ruby 2.4.4, GraalVM CE Native [x86_64-darwin]

$JAVA_HOME/jre/bin/js --version
Graal JavaScript 1.0 (GraalVM CE Native 1.0.0-rc9)

$JAVA_HOME/jre/bin/node --version
v10.9.0

Truffle und Graal

Der eigentliche Star in diesem Thema ist das Sprachwerkzeug Truffle, das es erlaubt, die Interpretationsinstruktionen für eine komplette (dynamische) Sprache in Java zu implementieren. Dabei werden Sprachelemente (Knoten im Syntaxbaum) durch annotierte Java-Methoden umgesetzt, zum Beispiel eine Zuweisung, Objekterzeugung oder Multiplikation.

sl13
Figure 2. Graal-Compiler, Truffle, Sulong pro Sprachfamilie - Quelle GraalVM Slides Oleg Shelajev

Damit kann die gesamte Sprache mittels Truffle interpretiert werden, nicht nur komplette Sprachen, auch beliebige DSLs wie z.B. Build-Tools oder Abfragesprachen sind möglich.

Das klingt erstmal eher akademisch und ziemlich langsam, aber da es in Java implementiert ist, stehen uns alle Tools wie IDE-Unterstützung, Unit-Tests, Debugging, Refactoring, Garbage-Collection und vieles mehr zur Verfügung. Auch sind viele Kernbestandteile zwischen Sprachen ähnlich (z.b. Ausdrücke, Typesysteme, Variablenhandhabung, Funktions- und Objekterzeugung), so dass bei einem sauberen aber mächtigem Typsystem grosse Teile des Truffle-Codes nachgenutzt werden können. Ausserdem hat ein gemeinsam genutztes Typsytem zur Folge, dass der Austausch zwischen den verschiedenen Sprachen mittels derselben Objekttypen und Instanzen erfolgt und deutlich einfacher wird.

Um jetzt diese saubere aber langsame Sprachimplementierung in eine effiziente Laufzeitumgebung zu verwandeln, kommt Graal in Spiel.

Dabei gibt es noch einen weiteren Trick in der Kiste - Spezialisierungen. Dieser Ansatz wird von allen modernen Laufzeitumgebungen genutzt, ich habe ihn in meinem Nashorn Artikel [Nashorn] im Detail erklärt. Man nimmt an, dass Code in Programmen nicht wirklich dynamisch ist, sondern Operationen an bestimmten Stellen nach einer Intialisierungsphase mit stabilen Typen (z.b. immer String und Integer) aufgerufen werden. Wenn jetzt in der Sprachdefinition für diese Typkombination eine spezialisierte Implementierung vorliegt, die auf effizientere Operatoren des Prozessors abgebildet werden kann, dann kann ein optimierender Compiler diese einsetzen. Zumindest solange die Typ-Annahme hält. Wenn das nicht mehr der Fall ist, fällt man zurück auf eine Interpretation oder eine "generische" Operation und kann nach einigen Durchläufen eine neue Annahme nutzen.

Mit den aggressiven Optimierungen von Graal, werden Objekte zu primitiven Werten, Streams zu For-Loops und unnützer Code entfernt. So dass zum Schluss nur noch die minimale Essenz des Programms übrig bleibt, die effzienter ausgeführt werden kann.

Damit kann die massive Leistungssteigerung (je nach Sprache und Kontext von bis zu 10x [FastR]) gegenüber den originalen Interpretern erklärt werden.

Wie auf der Abbildung 2 zu sehen, compiliert Graal JVM Bytecode, damit werden alle JVM Sprachen abgedeckt. Dynamische Sprachen wie JavaScript, Ruby, Python und R sind in Truffle implementiert und LLVM Bitcode (und damit Swift, Rust, C++ usw) wird über das Sulong Projekt integriert.

Beste JavaScript Unterstützung ist einer der aktuelln Schwerpunkte in der Entwicklung von Graal. Node-Anwendungen laufen damit schon gut, ECMAscript 6, 7 und 2018 werden voll unterstützt. Die Implementierung ist auch als eine JSR-223 Script-Engine namens "graal.js" verfügbar. Einer der Gründe dafür ist, dass Nashorn als JavaScript Implementierung auf der JVM ersetzt werden soll, da die Wartung dieser Implementierung zu aufwändig ist. In Truffle muss (neben der Parseranpassung) für ein neues Sprachfeature nur annotierte Java-Methoden hinzugefügt werden, was die Sprachanpassungen deutlich leichter macht.

Auch native Bibliotheken können genutzt werden, da Graal ja auf dem Maschinencode-Level interagiert. Die Leistung der Graal basierten Javascript Runtime kann mit V8 fast mithalten.

Polyglot in Aktion

Für die Nutzung anderer Sprachen auf der JVM steht eine API bereit, die in ähnlicher Weise auch in den anderen Sprachen genutzt werden kann. Dabei ist vorteilhaft, dass in der Truffleimplementierung ein gemeinsames Typsystem genutzt wird, was den Austausch von Datenstruktur-Instanzen ohne Konvertierung ermöglicht.

Mit den Context, Source, Polyglot und Value [APIs] können die meisten Anforderungen schon erfüllt werden. Im Context kann man sowohl Fragmente, als auch Dateien ausführen, dieser stellt dann auch ggf. definierte Funktionen und Objekte zum Zugriff bereit.

Value erlaubt es Parameter und Ergebnisse polyglotter Aufrufe korrekt zu behandeln. Man kann die Werte als primitive Typen, Arrays, Listen, Maps, Objekte, und Funktionen lesen und schreiben, oder z.b. auf Elemente zugreifen.

Ich nutze in diesem Beispiel ein Groovy-Skript. Dank @Grab Annotation ziehen es sich seine Abhängigkeiten selbst (notwendig in OpenJDK11).

Beispiel für Polygotte APIs - Answer.groovy
@Grab("org.graalvm.sdk:graal-sdk:1.0.0-rc11")
@Grab("org.graalvm.truffle:truffle-api:1.0.0-rc11")
@Grab("org.graalvm.js:js:1.0.0-rc11")

import org.graalvm.polyglot.*

ctx = Context.newBuilder().allowAllAccess(true).build()
value = ctx.eval("js", "10+Math.pow(2,3)*4")
println(value.asInt())

value = ctx.eval("js", "[10, Math.pow(2,3)*4]")
println(value.getArrayElement(0).asInt() * value.getArrayElement(1).asInt())

mul = ctx.eval("js","function(v) {return v*v;}").as(Function.class)
println(mul.apply(9))
Ausführung mit OpenJDK 11
java -version
openjdk version "11.0.1" 2018-10-16
OpenJDK Runtime Environment 18.9 (build 11.0.1+13)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13, mixed mode)

JAVA_OPTS="-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler" groovy Answer.groovy
42
320
81

In der GraalVM mit installierten dynamischen Sprachen kann ich auch folgenden Test ausführen, ohne die Abhängigkeiten in den Klassenpfad setzen zu müssen.

Testprogramm in Java für alle unterstützten Sprachen
import org.graalvm.polyglot.*;

try (Context ctx = Context.newBuilder().allowAllAccess(true).build()) {
  ctx.eval("js", "print('Hello JavaScript!');");
  ctx.eval("R", "print('Hello R!');");
  ctx.eval("ruby", "puts 'Hello Ruby!'");
  ctx.eval("python", "print('Hello Python!')");
}

Sofern in den Runtimes mittels Paketmanagern die notwendigen Bibliotheken für z.B. wissenschaftliche Berechnungen oder Diagrammdarstellung installiert wurden, können sie auch im polyglotten Aufruf genutzt werden.

Poly-Polyglot

Auch in den unterstützten dynamischen Sprachen kann diese Graal-API genutzt werden, um Zugriff auf alle anderen Sprachen zu erhalten. Dazu muss in der jeweiligen Runtime das --polyglot Flag aktiviert werden. Die Integration von Java kann direkt erfolgen, wenn man ---jvm nutzt.

In der polyglot API stehen eval zum Ausführen, export und import zum Bereitstellen bzw. zur Nutzung von Variablen zur Verfügung.

Polyglottes JavaScript Programm das Java Klassen direkt benutzt
// Ausführung: js --jvm --polyglot test.js
const BigInteger = Java.type("java.math.BigInteger")

let a = new BigInteger(10).add(new BigInteger(8).multiply(new BigInteger(4)))
console.log(a)

Hier sehen wir den Aufruf von "R" Code in Graal-Python

polyglot.py
import polyglot
rcode = "paste('Graal 1.0', 'RC', c(1:9), sep=' ')"
versions = polyglot.eval(string=rcode, language="R")
print("Available Versions of Graal", versions)
Ausführung des polyglotten Python Programms
$JAVA_HOME/bin/graalpython --polyglot --jvm polyglot.py
Available Versions of Graal ['Graal 1.0 RC 1', 'Graal 1.0 RC 2', 'Graal 1.0 RC 3', 'Graal 1.0 RC 4',
'Graal 1.0 RC 5', 'Graal 1.0 RC 6', 'Graal 1.0 RC 7', 'Graal 1.0 RC 8', 'Graal 1.0 RC 9']

Und umgekehrt den Aufruf einer Python-Funktion in FastR.

polyglot.r
pycode <-
"def fac(n):
    if n==1: return 1
    else: return n*fac(n-1)

fac
"
fac <- eval.polyglot("python",pycode)
print(fac(5))
Ausführung des polyglotten R Programms
$JAVA_HOME/bin/Rscript --polyglot polyglot.r
[1] 120

Ein nettes Beispiel aus der Graal Dokumentation, ist eine mehrsprachige, interaktive Shell, die erstaunlich gut funktioniert. Für die verfügbaren Sprachen kann man Fragmente eingeben, die dann direkt ausgeführt werden. Mit Eingabe des Sprachnamens wird die Sprache gewechselt.

Shell.java
import java.io.*;
import java.util.*;
import org.graalvm.polyglot.*;
import static java.lang.System.out;

public class Shell {

  public static void main(String[] args) throws Exception {
    BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
    Context context = Context.newBuilder().allowAllAccess(true).build();

    Set<String> languages = context.getEngine().getLanguages().keySet();
    String language = languages.iterator().next();

    out.println("Shell for " + languages + ":");
    while (true) {
      try {
        out.print(language + "> ");
        String line = input.readLine();
        if (line == null) break;
        else if (languages.contains(line)) language = line;
        else {
          Source source = Source.newBuilder(language, line, "<shell>")
                          .interactive(true).buildLiteral();
          context.eval(source);
        }
      } catch (PolyglotException pe) {
          if(pe.isExit()) break;
          out.println(pe.getMessage());
      }
    }
  }
}

Fazit

Graal hat eine beachtliche Entwicklung gemacht, es ist definitiv zum empfehlen, zumindest den Graal-JIT in Java 11 zu testen. Die polyglotten Features sind wirklich beeindruckend, besonders in ihrer Kompatibilität zwischen den Sprachen, und die Verfügbarkeit in nativen Binaries. Ich denke dass wir im nächsten Jahr auch mit dem 1.0 Release eine breite Anwendung der Technologie sehen werden, für dynamische Sprachen auf der JVM und im Data-Science Umfeld (vielleicht Graal-Python/FastR in Spark?). Auch die Unterstützung von LLVM Bitcode ist ein cleverer Schachzug, da damit eine Reihe weiterer Sprachen unterstützt werden kann, die sogar in einer sichereren Sandbox ausgeführt werden können. Insgesamt bin ich von der Arbeit des Graal Teams echt beeindruckt, weiter so!

Referenzen

Espresso Stampfer - Vorkompilierte Programme mit GraalVM AOT

In diesem zweiten Artikel zu Graal, soll es vor allem um 2 Themen gehen.

Zuerst möchte ich die polyglotten Programmierung auf der GraalVM abrunden und neben Fehlersuche und LLVM Integration, ein praktisches Beispiel aus meinem Umfeld darstellen. Wir wollen die JVM-basierte GraphDatenbank Neo4j um benutzerdefinierte Funktionen und Prozeduren anreichern, die nicht nur in den klassischen JVM Sprachen wie Java, Scala oder Kotlin geschrieben sind, sondern in einer der unterstützten Sprachen der GraalVM, also JavaScript, Ruby, Python und R.

In der zweiten Hälfte möchte ich zeigen, wie man mit den Werkzeugen der GraalVM, die eigene Anwendung schon zum Buildzeitpunkt zu Maschinencode optimieren kann. Dass dabei eine komplette JVM, Klassenbibliotheken und der eigene Bytecode zu einer schnell startenden, nativen Anwendung zusammengeschrumpft werden, ist schon ziemlich beachtlich. Der Hauptvorteil dieses Ansatzes liegt in der erheblich kürzeren Startzeit und dem geringeren Speicherverbrauch der Anwendung.

Aktuelles

Seit dem letzten Artikel ist die Zeit im Graal Projekt nicht stehengeblieben, mittlerweile ist Graal bei der Version 1.0-RC11 angekommen. Dabei hat sich auch der Name der Graal-JVM in SDKMAN geändert auf: 1.0.0-rc-11-grl

Einige Verbesserungen [Release Notes] in den letzten Versionen umfassen:

  • Deutlich verbesserte Python Unterstützung, inklusive eines eigenen Installers namens ginstall

  • Neue Option --allow-incomplete-classpath für native Images, um Bibliotheken, wie Spring-Boot, die Laufzeitkonfiguration von der (Nicht-)Existenz von Klassen abhängig machen, besser zu unterstützen

  • Neue Sicherheitsoptionen für Sandboxing von z.B. eval

Polyglotte Anwendungen mit GraalVM

Fehlersuche

Um Fehler oder Probleme in polyglotte Anwendungen zu finden, bringt GraalVM zum einen ein angepasstes jVisualVM Tool mit, das mit den verschiedenen Kontexten umgehen kann und zum Beispiel die Heaps der verschiedenen Sprachen anzeigt.

Ein kompletter polyglotter Debugger, der die Google Chrome Debugger UI benutzt, ist nutzbar, wenn man bei der Programmausführung das --inspect Flag nutzt. Dann wird eine lokale URL ausgegeben, die man in Chrome öffnen kann.

Neben dem Debuggen jedes polyglotten Fragments neben dem Hauptprogramm zeigt diese Integration auch polyglotte Stacktraces.

graal chrome debugger

Mittels der --cpusampler --cpusampler.Mode=statements Flags erhält man CPU Samples für das eigene Programm.

Ausgabe CPU Samples
 --------------------------------------------------------------------------------------------
 Sampling Histogram. Recorded 39 samples with period 1ms
   Self Time: Time spent on the top of the stack.
   Total Time: Time the location spent on the stack.
   Opt %: Percent of time spent in compiled and therfore non-interpreted code.
 --------------------------------------------------------------------------------------------
  Thread: Thread[main,5,main]
  Name       |      Total Time     |  Opt % ||   Self Time     |  Opt % | Location
 --------------------------------------------------------------------------------------------
  :program~6 |         21ms  53.8% |   0.0% ||     21ms  53.8% |   0.0% | test.js~6:117-189
  :program~4 |         10ms  25.6% |   0.0% ||     10ms  25.6% |   0.0% | test.js~4:65-110
  :program~7 |          8ms  20.5% |   0.0% ||      8ms  20.5% |   0.0% | test.js~7:191-204
  :program   |         39ms 100.0% |   0.0% ||      0ms   0.0% |   0.0% | test.js~1-7:0-204
 --------------------------------------------------------------------------------------------

Wer unter die Haube schauen möchte, kann sich den generierten Operationsbaum von Graal in einer Netzwerkdarstellung im [Ideal] Visualisierungswerkzeug anzeigen lassen. Dazu muss man das Tool installieren und starten und kann dann seiner Anwendung die Flags -Dgraal.Dump= und optional -Dgraal.MethodFilter=MyProgram.* mitgeben.

average

LLVM Unterstützung

Die Entscheidung, auch LLVM Bitcode zu unterstützen, erlaubt es viele weitere Sprachen, die mittel der LLVM-Werkzeuge verarbeitet werden können, zu nutzen.

Hier ist ein Beispiel das C-Code und eine notwendige Bibliothek (libcurl) einbindet und ausführt.

use-curl.c
#include <stdio.h>
#include <curl/curl.h>

long request() {
    CURL *curl = curl_easy_init();
    long response_code = -1;

    if(curl) {
      CURLcode res;
      curl_easy_setopt(curl, CURLOPT_URL, "http://example.com");
      res = curl_easy_perform(curl);
      if(res == CURLE_OK) {
        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
      }
      curl_easy_cleanup(curl);
    }
    return response_code;
}
CurlTest.groovy
import org.graalvm.polyglot.*

polyglot = Context.newBuilder()
    .allowAllAccess(true)
    .option("llvm.libraries", "/usr/lib/libcurl.dylib")
    .build()
source = Source
    .newBuilder("llvm", new File("use-curl.bc"))
    .build()

result = polyglot.eval(source)

responseValue = result.getMember("request").execute()
responseCode = responseValue.asLong()

print(responseCode)
Mit clang LLVM Bitcode erzeugen und "CurlTest" Programm mittles Groovy ausführen
clang -c -O1 -emit-llvm use-curl.c
groovy CurlTest.groovy

Ein interessantes Feature ist "Sandboxing" [Sandbox]. Damit kann der Bitcode mit den Sicherheitsgarantien der JVM ausgeführt werden, so gibt es Schutz vor Null-Pointer-, ungültigen Feld- und Speicherzugriffen und vieles mehr. Das ist aber ein Feature der Enterprise Edition von GraalVM!

Polyglotte Datenbankprozeduren

Wie im ersten Artikel angemerkt, kann Graal in Datenbanken wie Oracle [JS-Oracle] und MySQL für polyglotte Implementierungen von nutzerdefinierten Funktionen und Prozeduren eingebettet werden. Viel leichter ist es natürlich in Datenbanken die selbst in Java implementiert sind, was gerade bei NoSQL Datenbanken oft der Fall ist.

Mein Beispiel ist Neo4j, das seit drei Jahren (Version 3.0) benutzerdefinierte Prozeduren unterstützt. Normalerweise müssen die in einer JVM Sprache geschrieben werden, aber dank der Graal-APIs können wir jetzt jede der unterstützten Sprachen nutzen.

Hier ist der Kern der Implementierung zu sehen:

PolyglotFunctions.java
@Context GraphDatabase graphDb;
Map<String, Source> functions = new ConcurrentHashMap<>();

@UserFunction("functions.define")
public void function(String name, String lang, String code) {
    Source source = Source.newBuilder(lang, code).build();
    functions.put(name,source);
}

@UserFunction("functions.run")
public Object run(String name, Map<String,Object> args) {
    try (Context context = Context.newBuilder().build()) {
        context.export("label",Label::label);
        context.export("type",RelationshipType::withName);
        context.export("db",graphDb);
        context.export("args",args);
        Source source = functions.get(name);
    	return context.eval(source, params);
    }
}

Wenn man jetzt Neo4j mittels GraalVM oder OpenJDK 11 mit den genannten Optionen ausführt, können diese Prozeduren genutzt werden, um z.B. dynamisch Funktionen in JavaScript, R, Ruby oder Python zu definieren und zu nutzen.

Polyglotte Funktion in Neo4j’s Cypher anlegen und aufrufen
CALL functions.define("avgFriendAge","js",
   "const ages = db.findNode(label('Person'),'id',args.get('id'))
      .getRelationships(type('FRIEND'))
      .map(rel => rel.endNode().getProperty('age'))
    ages.reduce((agg,age) => agg + age)/ages.length");

CALL functions.run("avgFriendAge", {id:"007"});

Ahead-of-Time Kompilierung (AOT)

AOT-Kompilierung selbst ist ein größeres Thema, daher kann ich hier nur einen Überblick geben, wie und warum man sie mit Graal nutzen kann. Den Maschinencode den ein JIT-Compiler normalerweise aus Bytecode zur Laufzeit erstellt, kann schon im Buildprozess erzeuget und als ausführbare Binärdateien gespeichert werden.

Das ist der Zweck des native-image Tools der GraalVM. Von diesem wird eine Java-Main Klasse, ihre genutzten Abhängigkeiten, Bibliotheken, JDK Funktionen und die relevanten Teile einer in Java implementierten JVM (SubstrateVM) in minimierten, optimierten Maschinencode überführt und als Binary gespeichert. Besonder interessant war für mich, dass die SubstrateVM früher oft als irrelevantes Spielzeug belächelt, jetzt mit dem aggressiven Graal-Compiler aber zum essentiellen Bestandteil des Lösungsansatzes wurde.

graal aot

Dieser Buildprozess ist ziemlich aufwändig und kann für komplexere Anwendungen schon ein längere Zeit dauern [AOT-Hands-On]. Hier ist ein Beispiel das rekursiv die Größe eines Verzeichnisses ermittelt, und so etwas wie das Kommandozeilentool du darstellen könnte.

Files.java
public class Files {

  public static void main(String[] args) {
  	File root = new File(args[0]);
    System.out.printf("Total size: %d %n",fileSizes(root));
  }
  private static long fileSizes(File file) {
  	if (file.isDirectory()) {
      return Stream.of(file.listFiles()).mapToLong(Files::fileSizes).sum();
  	}
    return file.length();
  }
}
Binary erstellen mit native-image
javac Files.java

native-image Files

Build on Server(pid: 22229, port: 54112)
[files:22229]    classlist:     174.19 ms
[files:22229]        (cap):     901.95 ms
[files:22229]        setup:   1,179.42 ms
[files:22229]   (typeflow):   2,885.87 ms
[files:22229]    (objects):   1,224.52 ms
[files:22229]   (features):      40.28 ms
[files:22229]     analysis:   4,224.40 ms
[files:22229]     universe:     120.25 ms
[files:22229]      (parse):     312.36 ms
[files:22229]     (inline):     531.09 ms
[files:22229]    (compile):   2,065.76 ms
[files:22229]      compile:   3,111.69 ms
[files:22229]        image:     611.66 ms
[files:22229]        write:     321.93 ms
[files:22229]      [total]:   9,907.16 ms
Test JIT vs. AOT
# GraalVM
time java Files .
Total size: 13110831
real	0m0.170s

# Zulu
time java Files .
Total size: 13110831
real	0m0.230s

# AOT image
time ./files .
Total size: 13110831
real	0m0.010s

# Natives Unix Tool
time du -sh .
 13M	.
real	0m0.004s

Dabei werden folgende Hauptziele erreicht - sehr schnelle Startzeit, meist im einstelligen Millisekundenbereich, und deutlich weniger Speichernutzung durch die Entfernung nicht erreichbaren Codes und Verzicht auf Klassen-Metainformationen.

Hauptanwendungszwecke sind:

  • Kommandozeilentools

  • Microservices

  • Funktionen

  • Binärbibliotheken

Ein sehr cooler Einsatzzweck sind ist die Optimierung von in Java implementierten, komplexen Entwicklerwerkzeugen wie Compiler (Scala, Clojure, Kotlin), Build-Tools (Maven, Gradle) oder Checker (Findbugs, Checkstyle) die oft und schnell aufgerufen werden sollen. Da Graal die objektorienterten bzw. funktionalen Ansätze in einigen dieser Sprachen besonders gut optimiert, bekommt man auch noch eine verbesserte Leistung. Für den Scala Compiler wurde in Benchmarks [ScalaC] eine Leistungssteigerung bis zu 30% nachgewiesen.

Für Microservice-Frameworks wie Spring-Boot, Micronaut, vert.x, Fn-Project wird die Erzeugung solcher Binaries aktiv unterstützt bzw. entwickelt. Dort ist gerade bei der Erstellung von Docker-Images die Platzersparnis durch die Wegoptimierung von JVM/JDK erheblich, in den meisten Fällen Faktor 10 oder mehr.

Die Nutzung polyglotten Features von Graal in der Host-Sprache ist auch in diesem Modus verfügbar. Das wird vom Graal Projekt selbst genutzt, um effiziente, schnell startende Laufzeitumgebungen für diese dynamischen Sprachen mitzuliefern. Diese haben dann zwar nicht die maximmale Laufzeitperformanz aber eine sehr kurze Startzeit.

Das native-image Tool führt bei der Generierung auch schon die Initialisierung von Klassen und deren statischer Felder und Blöcke durch, und speichert die Informationen im "image heap". Damit wird dies nicht zum Start des Programms notwendig.

Das muss man bedenken, wenn der eigene Code zum Zeitpunkt der statischen Klassen-Initialisierung schon aktiv wird und zum Beispiel Threads startet, Sockets öffnet, Speicher alloziert, Konfiguration liest usw. Wenn man diese Optimierung nicht nutzen kann oder will, ist es möglich die Initialisierung dieser Klassen mit --delay-class-initialiazation-to-runtime=class,list zu verzögern.

Es gibt einige Einschränkungen [AOT-Limits] der Substrate-VM und des statischen Binärcodes, aber auch Workarounds bzw. Konfigurationsoptionen dafür, z.B. Auflistung von Klassen und Methoden für Reflection (-H:ReflectionConfigurationFiles=json-files), Nutzung von Unsafe, JNI, Laden von Klassen, usw. Wenn man damit leben kann, dass diese Einschränkungen erst zur Laufzeit zum Fehler führen, kann -H:+ReportUnsupportedElementsAtRuntime genutzt werden. Um Resourcen in das Binärfile zu integrieren, kann man sie mittels: -H:IncludeResources=regexp angeben, zum Beispiel: -H:IncludeResources=application.yml|META-INF/services/.+

Eine sehr detaillierte Detektivgeschichte zur Anwendung dieser Optionen und Workarounds kann man in [AOT-Hands-On] miterleben.

Man darf nicht vergessen dass das erzeugte Binary weniger optimal ist, als im JIT, da die ganzen Laufzeit-Profilinformationen über die eigentliche Nutzung des Codes fehlen.

Aber erhält man optimierten Code, der viel weniger Speicher belegt - da u.a. die Klassenmetainformationen nicht gespeichert werden müssen und durch die Codeeliminierung nur das minimale Gerüst, das für unser Programm wirklich notwendig ist, bereitgestellt wird.

Wie schon erwähnt, ist dieser Ansatz besonders für die Erstellung von Container-Images interessant.

Während sonst Java Anwendungen mehrere hundert Megabyte große Images erfordern, kommen statisch gelinkte, native Binaries mit wenigen Megabytes aus. Statt auf einem minimalen "Alpine" System zu basieren kann man einfach ein leeres scratch benutzen.

Dazu muss die erzeugte Binärdatei natürlich auf Linux ausführbar sein, wofür man auch wieder einen Docker Container nutzen kann der die Linux Version von GraalVM’s native-image enthält.

native-image/Dockerfile Buildfile um Linux Binaries auf anderen Systemen zu erzeugen.
FROM ubuntu

RUN apt-get update && \
    apt-get -y install gcc libc6-dev zlib1g-dev curl bash && \
    rm -rf /var/lib/apt/lists/*

ENV GRAAL_VERSION 1.0.0-rc11
ENV GRAAL_FILENAME graalvm-ce-${GRAAL_VERSION}-linux-amd64.tar.gz

# GraalVM herunterladen und auspacken
RUN mkdir -p /usr/lib/graalvm
RUN curl -4 -L https://github.com/oracle/graal/releases/download/vm-${GRAAL_VERSION}/${GRAAL_FILENAME} -o - |\
    tar xzf - --strip-components 1 -C /usr/lib/graalvm

# Arbeitsverzeichnis anlegen
VOLUME /project
WORKDIR /project

# Ausführung
ENTRYPOINT ["/usr/lib/graalvm/bin/native-image","--static"]

Wenn man später die Fehlermeldung standard_init_linux.go:190: exec user process caused "no such file or directory" bekommt, dann fehlen Standardbibliotheken im "nackten" Image. Dann hat man das --static bei der Erzeugung vergessen und stattdessen eine dynamisch gelinkte Datei erzeugt.

Dann können wir dieses graalvm-native Docker Image nutzen, um unser Linux-Binary zu bauen und es dann in das minimale Docker Image einpacken und ausführen.

Minimales Docker Buildfile für unsere Binärdatei
FROM scratch
VOLUME /folder

ADD files /
CMD ["/files","/folder"]
Ablauf der Erzeugung der Docker-Images und Ausführung unserer Binärdatei
docker build native-image -t graalvm-native
docker run --rm -v `pwd`:/project graalvm-native Files
Build on Server(pid: 12, port: 40339)*
[files:12]    classlist:   2,537.64 ms
...

# Nicht ausführbar da Linux Binary
./files
./files: cannot execute binary file

# Minimales Dockerfile bauen
docker build . -t files
# Unser Image ist nur 9.61MB gross

docker run -v /tmp:/folder --rm files
Total size: 3086078

Fazit

GraalVM ist wie eine Wundertüte [10-Things], egal an welcher Ecken man anfängt zu stöbern, man findet faszinierende Technologien und Lösungen, die aufeinander aufbauen und sich ergänzen. Das Team um Thomas Wuerthinger hat wirklich ganze Arbeit geleistet. Ich bin gespannt wie sich die Abdeckung der Sprachen (besonders Python für Data-Science) entwickelt und freue mich auf das 1.0 Release. Bei Neo4j werden wir weiter die Möglichkeiten von GraalVM für polyglotte Interaktion mit der Datenbank und Graphalgorithmen ausloten.

Referenzen