Slide 1

Slide 1 text

Your Program as a Transpiler Improving Application Performance by Applying Compiler Design

Slide 2

Slide 2 text

About Me • Edoardo Vacchi @evacchi • Research @ UniMi / Horsa • Research @ UniCredit R&D • Drools and jBPM Team @ Red Hat

Slide 3

Slide 3 text

Motivation

Slide 4

Slide 4 text

Motivation • Language implementation is often seen as a dark art • But some design patterns are simple at their core • Best practices can be applied to everyday programming

Slide 5

Slide 5 text

Motivation (cont'd) • As GraalVM and becomes more and more relevant, thinking differently of our own code can buy us quick performance wins

Slide 6

Slide 6 text

Boot-time vs. Run-time • There is always a pre-processing phase where you prepare your program for execution • Then, there's actual process execution phase

Slide 7

Slide 7 text

Goals 1. How can we factor pre-processing out of program run-time ? 2. Can we factor it out of the program ?

Slide 8

Slide 8 text

Example: A Quick DI Framework https://github.com/evacchi/reflection-vs-codegen public class Example { private final Animal animal; @Inject public Example(Animal animal) { this.animal = animal; } public Animal animal() { return animal; } } public interface Animal {} @InjectCandidate public class Dog implements Animal {}

Slide 9

Slide 9 text

Binder binder = new Binder(); binder.scan(); Example ex = binder.createInstance(Example.class); Animal animal = ex.animal(); Objects.requireNonNull(animal); assert animal instanceof Dog https://github.com/evacchi/reflection-vs-codegen Vaguely inspired by Guice https://github.com/google/guice

Slide 10

Slide 10 text

public class Binder { public Binder scan() { Reflections reflections = new Reflections(); reflections.getTypesAnnotatedWith(InjectCandidate.class) .forEach(t -> bindings.put(interfaceOf(t), constructorOf(t))); return this; } public T createInstance(Class extends T> t) { return (T) Arrays.stream(t.getDeclaredConstructors()) .filter(c -> c.getAnnotation(Inject.class) != null) .peek(c -> c.setAccessible(true)) .map(this::createInstance) .findFirst().get(); } ... } https://github.com/evacchi/reflection-vs-codegen

Slide 11

Slide 11 text

Example: Boot Time ︎% time java io.github.evacchi.Reflective 6.94s user 0.29s system 259% cpu 2.785 total • Not much, but not great • Might be fine for long-running • Not great for microservices or serverless

Slide 12

Slide 12 text

GraalVM

Slide 13

Slide 13 text

GraalVM: “One VM to Rule Them All” • Polyglot VM with cross-language JIT • Java Bytecode and JVM Languages • Dynamic Languages (Truffle API) • Native binary compilation (SubstrateVM)

Slide 14

Slide 14 text

GraalVM: “One VM to Rule Them All” • Polyglot VM with cross-language JIT • Java Bytecode and JVM Languages • Dynamic Languages (Truffle API) • Native binary compilation (SubstrateVM)

Slide 15

Slide 15 text

Native Image % javac A.java ︎ % time java A hello java A 0.09s user 0.02s system 39% cpu 0.267 total ︎ % native-image A ... ︎ % time ./a hello ./a 0.00s user 0.00s system 86% cpu 0.001 total public class A { public static void main(String[] args) { System.out.println("hello"); } }

Slide 16

Slide 16 text

Our DI Framework breaks % native-image io.github.evacchi.Reflective Build on Server(pid: 24595, port: 42437)* [io.github.evacchi.reflective:24595] classlist: 11,753.18 ms [io.github.evacchi.reflective:24595] (cap): 1,514.71 ms [io.github.evacchi.reflective:24595] setup: 4,127.10 ms [io.github.evacchi.reflective:24595] analysis: 1,006.40 ms Fatal error: com.oracle.svm.core.util.VMError$HostedError: should not reach here at com.oracle.svm.core.util.VMError.shouldNotReachHere(VMError.java:62) ... Error: Image build request failed with exit status 1

Slide 17

Slide 17 text

Native Image: Restrictions • Native binary compilation • Restriction: "closed-world assumption" • No dynamic code loading • You must declare classes upon which you plan to do reflection

Slide 18

Slide 18 text

Transpilers

Slide 19

Slide 19 text

Transpilers vs. Compilers • Compiler: translates code written in a language (source code) into code written in a target language (object code). The target language may be at a lower level of abstraction • Transpiler: translates code written in a language into code written in another language at the same level of abstraction (Source-to-Source Translator).

Slide 20

Slide 20 text

Are transpilers simpler than compilers? • Lower-level languages are complex • They are not: if anything, they're simple • Syntactic sugar is not a higher-level of abstraction • It is: a concise construct is expanded at compile-time • Proper compilers do low-level optimizations • You are thinking of optimizing compilers.

Slide 21

Slide 21 text

The distinction is moot • It is pretty easy to write a crappy compiler, call it a transpiler and feel at peace with yourself • Writing a good transpiler is no different or harder than writing a good compiler • So, how do you write a good compiler?

Slide 22

Slide 22 text

Your Program as a Compiler Improving Application Performance by Applying Compiler Design

Slide 23

Slide 23 text

Compiler-like workflows • At least two classes of problems can be solved with compiler-like workflows • Data transformation problems • Boot time optimization problems

Slide 24

Slide 24 text

Compiler-like workflows • At least two classes of problems can be solved with compiler-like workflows • Data transformation problems • Boot time optimization problems

Slide 25

Slide 25 text

Step 1 Recognize your compilation phase

Slide 26

Slide 26 text

What's a compilation phase? • It's your setup phase. • You do it only once before the actual processing of your program begins • But do you have to do it every single time it starts?

Slide 27

Slide 27 text

Configuring the application • Will that configuration change across runs? • Do you have to repackage the application to bundle the new configuration?

Slide 28

Slide 28 text

Application wiring • You are building an immutable Dockerized microservice • Do you really need all that Runtime Reflection? • Do you really need Runtime Dependency Injection? public class Example { private final Animal animal; @Inject public Example(Animal animal) { this.animal = animal; } public Animal animal() { return animal; } } public interface Animal {} @InjectCandidate public class Dog implements Animal {}

Slide 29

Slide 29 text

All these things make your startup slow! • But it's done only once! • Never is better than once • But it's flexible • Ask yourself when is the last time you changed dependencies/startup config/classpath at runtime • If it's recent, ask yourself the price you pay for that flexibility

Slide 30

Slide 30 text

Step 2 Work like a compiler

Slide 31

Slide 31 text

Compiling a programming language • You start from a text representation of a program • The text representation is fed to a parser • The parser returns a parse tree • The parse tree is refined into an abstract syntax tree (AST) • The AST is further refined through intermediate representations (IRs) • Up until the final representation is returned

Slide 32

Slide 32 text

Compiling a programming language • You start from a text representation of a program • The text representation is fed to a parser • The parser returns a parse tree • The parse tree is refined into an abstract syntax tree (AST) • The AST is further refined through intermediate representations (IRs) • Up until the final representation is returned

Slide 33

Slide 33 text

Recognize your compiler passes 1. Collect your resources 2. Find all the dependencies between resources 3. Build a data structure representation (e.g. a graph) 4. Visit the resulting structure as many times as you like 5. Generate the (source) code

Slide 34

Slide 34 text

What makes a compiler a proper compiler • Not optimization • Compilation Phases • You can have as many as you like

Slide 35

Slide 35 text

Compilation Phases • Misconception: one pass doing many things is better than doing many passes, each doing one thing • It is not: the complexity is the same

Slide 36

Slide 36 text

Compilation Phases • Better separation of concerns • Better testability (you can test each intermediate result) • You can choose when and where each phase gets evaluated

Slide 37

Slide 37 text

Example. A Configuration File 3 Resolve includes 2 Unmarshall file into a typed object 1 Read file from (class)path 5 Validate data types and values 4 Resolve variables

Slide 38

Slide 38 text

Example. An ORM Library 3 Fetch the DB Catalog 2 Find relationships between classes 1 Scan classpath for annotations 5 Synthesize entity implementations 4 Synthesize prepared statements

Slide 39

Slide 39 text

Example. A DI Framework 3 Verify all deps are satisfied 2 Find relationships between classes 1 Scan classpath for annotations 5 Synthesize factories 4 Find a cycle-free path public class Example { private final Animal animal; @Inject public Example(Animal animal) { this.animal = animal; } public Animal animal() { return animal; } } public interface Animal {} @InjectCandidate public class Dog implements Animal {}

Slide 40

Slide 40 text

Binder binder = new Binder(); binder.scan(); Example ex = binder.createInstance(Example.class); Animal animal = ex.animal(); Objects.requireNonNull(animal); assert animal instanceof Dog https://github.com/evacchi/reflection-vs-codegen

Slide 41

Slide 41 text

public class Binder { public Binder scan() { Reflections reflections = new Reflections(); reflections.getTypesAnnotatedWith(InjectCandidate.class) .forEach(t -> bindings.put(interfaceOf(t), constructorOf(t))); return this; } public T createInstance(Class extends T> t) { return (T) Arrays.stream(t.getDeclaredConstructors()) .filter(c -> c.getAnnotation(Inject.class) != null) .peek(c -> c.setAccessible(true)) .map(this::createInstance) .findFirst().get(); } ... } https://github.com/evacchi/reflection-vs-codegen At run-time, complexity is easy to miss This loop gets executed at each instance creation

Slide 42

Slide 42 text

public void scan() { Reflections reflections = new Reflections(); // resolve injection candidates reflections.getTypesAnnotatedWith(InjectCandidate.class); // resolve injected constructors reflections.getConstructorsAnnotatedWith(Inject.class); // collect candidates reflections.forEach(this::collect); // resolve mappings resolveMappings(); }

Slide 43

Slide 43 text

Compilation Phases • Phases are now more apparent • Run-time is not affected • Careful: classpath scanning is still costly • Avoid doing it more than once! • Do we need to scan() at each startup ?

Slide 44

Slide 44 text

Step 3 Generate code at compile-time

Slide 45

Slide 45 text

Code-generated DI GeneratedBinder binder = new GeneratedBinder(); Example ex = binder.createInstance(Example.class); Animal animal = ex.animal(); Objects.requireNonNull(animal); assert animal instanceof Dog; public class GeneratedBinder { public T createInstance(Class> type) { if (Example.class == type) return (T) new Example(new Dog()); if (Animal.class == type) return (T) new Dog(); throw new UnsupportedOperationException(); }} cf. https://google.github.io/dagger/ Binder binder = new Binder(); binder.scan(); Example ex = binder.createInstance(Example.class); Animal animal = ex.animal(); Objects.requireNonNull(animal); assert animal instanceof Dog

Slide 46

Slide 46 text

F O R T H E L O V E O F G O D DO NOT CONCATENATE STRINGS Seriously, Stop. You're Killing Kittens. Not Even StringBuilder

Slide 47

Slide 47 text

Use proper code generation tooling • Use a type-safe API to generate source or byte code • Java Parser provides code generation APIs • Java Poet • ByteBuddy • ASM • etc. • Don't like APIs? A templating engine is fine too • It won't be typesafe, but it's still ok • Hell, even String.format() is better

Slide 48

Slide 48 text

JavaParser private void generateJavaSources(Bindings bindings) { String packageName = "io.github.evacchi"; String className = "GeneratedBinder"; String sourceFileName = packageName + "." + className; CompilationUnit cu = new CompilationUnit(); ClassOrInterfaceDeclaration cls = cu .setPackageDeclaration("io.github.evacchi") .addClass(className); MethodDeclaration methodDeclaration = cls .addMethod("createInstance") .setTypeParameters(new NodeList<>(new TypeParameter("T"))) .setType("T") .setModifiers(Modifier.Keyword.PUBLIC) .addParameter(Class.class, "type"); ... } https://github.com/evacchi/reflection-vs-codegen

Slide 49

Slide 49 text

Write a build plug-in • A Maven/Gradle/SBT/Whatever Plug-In • An Annotation Processor • A Quarkus Extension

Slide 50

Slide 50 text

The processor is triggered by the Java compiler for claimed annotations. Bindings bindings = processInjectionCandidates( env.getElementsAnnotatedWith(InjectCandidate.class)); processInjectionSites( env.getElementsAnnotatedWith(Inject.class), bindings); generateJavaSources(bindings); https://github.com/evacchi/reflection-vs-codegen DI: Annotation Processor

Slide 51

Slide 51 text

Example % time java io.github.evacchi.Reflective 6.94s user 0.29s system 259% cpu 2.785 total % time java io.github.evacchi.Codegen 0.08s user 0.01s system 111% cpu 0.087 total

Slide 52

Slide 52 text

Example % time java io.github.evacchi.Reflective 6.94s user 0.29s system 259% cpu 2.785 total % time java io.github.evacchi.Codegen 0.08s user 0.01s system 111% cpu 0.087 total % time ./io.github.evacchi.codegen ./io.github.evacchi.codegen 0.00s user 0.00s system 86% cpu 0.003 total

Slide 53

Slide 53 text

Case Study

Slide 54

Slide 54 text

The Submarine Initiative “The question of whether a computer can think is no more interesting than the question of whether a submarine can swim.” Edsger W. Dijkstra

Slide 55

Slide 55 text

AI and Automation Platform • Drools rule engine • jBPM workflow platform • OptaPlanner constraint solver

Slide 56

Slide 56 text

Drools and jBPM rule R1 when // constraints $r : Result() $p : Person( age >= 18 ) then // consequence $r.setValue( $p.getName() + " can drink"); end Drools jBPM

Slide 57

Slide 57 text

Drools DRL rule R1 when // constraints $r : Result() $p : Person( age >= 18 ) then // consequence $r.setValue( $p.getName() + " can drink"); end var r = declarationOf(Result.class, "$r"); var p = declarationOf(Person.class, "$p"); var rule = rule("com.example", "R1").build( pattern(r), pattern(p) .expr("e", p -> p.getAge() >= 18), alphaIndexedBy( int.class, GREATER_OR_EQUAL, 1, this::getAge, 18), reactOn("age")), on(p, r).execute( ($p, $r) -> $r.setValue( $p.getName() + " can drink")));

Slide 58

Slide 58 text

jBPM RuleFlowProcessFactory factory = RuleFlowProcessFactory.createProcess("demo.orderItems"); factory.variable("order", new ObjectDataType("com.myspace.demo.Order")); factory.variable("item", new ObjectDataType("java.lang.String")); factory.name("orderItems"); factory.packageName("com.myspace.demo"); factory.dynamic(false); factory.version("1.0"); factory.visibility("Private"); factory.metaData("TargetNamespace", "http://www.omg.org/bpmn20"); org.jbpm.ruleflow.core.factory.StartNodeFactory startNode1 = factory.startNode(1); startNode1.name("Start"); startNode1.done(); org.jbpm.ruleflow.core.factory.ActionNodeFactory actionNode2 = factory.actionNode(2); actionNode2.name("Show order details"); actionNode2.action(kcontext -> {

Slide 59

Slide 59 text

Startup Time

Slide 60

Slide 60 text

Conclusion

Slide 61

Slide 61 text

Take Aways • Do more in the pre-processing phase (compile-time) • Do less during the processing phase (run-time) • In other words, separate what you can do once from what you have to do repeatedly • Process in phases • Move all or some of your phases to compile-time

Slide 62

Slide 62 text

Resources • Full Source Code https://github.com/evacchi/reflection-vs-codegen • KIE.org Drools, jBPM, OptaPlanner • Submarine https://github.com/kiegroup/submarine-examples • Drools Blog http://blog.athico.com • Other resources • GraalVM.org • Quarkus.io • Dagger https://google.github.io/dagger/ Edoardo Vacchi @evacchi

Slide 63

Slide 63 text

Q&A