Compiler optimalisaties

Article JAVA Magazine 04 – 2021

Misschien zijn compiler optimalisaties wel het minst interessant om te gebruiken, omdat als je performanceproblemen hebt, je deze vaker kunt vinden in database optimalisaties of op andere plekken. Desalniettemin is het interessant om te weten wat er allemaal onder de motorkap gebeurt in Java. In dit artikel gaan we een aantal stukjes code bekijken om te zien wat de Java compiler je biedt aan optimalisaties.

 

Om de code die je schrijft te kunnen draaien, moet eerst de source code gecompileerd worden. In Java gebeurt dat niet meteen naar machinecode die de CPU kan uitvoeren, maar wordt het eerst vertaald naar Java bytecode. Tijdens deze compilatie vinden een aantal kleine optimalisaties plaats. Daarna, als de code draait, wordt deze bytecode eerst geïnterpreteerd door de Java Runtime. Ook daar vinden wat optimalisaties plaats. Deze zijn vaak wat ‘slimmer’.

 

{ String concatenatie }

Laten we het volgende stukje code bekijken:

 

public class Strings {

private static final int number = 10;

public String getQ() {

String ql = “Do ” + number + ” times”;

}

}

 

Zouden we de code nog efficiënter kunnen maken? Je zou misschien denken dat het volgende helpt:

 

public String getQ() {

return “Do 10 times”;

}

 

Ok, je verliest wat flexibiliteit, omdat je number vastzet, maar dat is toch een constante. Gelukkig hoef je dit niet zelf te schrijven, de compiler doet dit voor je. Er is dus geen reden om een string die verdeeld is over meerdere regels en of andere constante zelf samen te voegen.

 

{ Final }

Misschien is final wel de meest bediscussieerde ‘modifier’ van Java onder collega’s. Moeten ze nu wel of niet? Laten we kijken naar het volgende stuk code:

 

String greet = “hello world”;

System.out.println(“Greet: ” + greet);

 

Zou het verschil maken als we de variabele greet final maken of niet?

 

We hebben met string concatenatie gezien dat de compiler in staat is om de string die meegegeven wordt aan println te concateneren, maar dat gebeurt alleen met constanten. De variabele greet is niet constant, dus de compiler zal de volgende code genereren:

 

System.out.println(new StringBuilder(“Greet: “)

.append(“hello world”)

.toString());

De StringBuilder word geïntroduceerd, omdat er twee strings worden samengevoegd. Onderwater in Java wordt de + vertaald naar een StringBuilder. Sinds Java 9 wordt het gedaan met de StringConcatFactory zie ook ‘JEP 280’, maar het principe is hetzelfde. Als greet final was geweest, was het volgende gegenereerd:

 

String greet = “hello world”;

System.out.println(“Greet: hello world”);

 

In dit geval heeft het final maken van een variabele dus wel degelijk effect. Je ziet misschien wel dat het definiëren van greet er nog steeds staat. Dit is dus eigenlijk ‘dode code’ geworden. De interpreter gaat dit voor je oplossen.

 

{ Dode code }

Als de interpreter code tegenkomt die geen (neven)effect heeft, zal deze niet worden omgezet naar machinecode om uit te voeren. Bij de code uit het vorige voorbeeld zal dus het toekennen van greet nooit echt worden uitgevoerd. Dit kan de interpreter doen met simpele gevallen. Als je een nieuw object aanmaakt, maar vervolgens niet gebruikt, wordt deze alsnog uitgevoerd. Dit omdat de interpreter niet weet wat de neveneffecten zijn.

 

{ String concatenatie in een loop }

Nu we een beetje doorhebben hoe de compiler omgaat met strings gaan we het in een loop doen:

 

String result = “”;

for (int i = 0; i <= 100; i++) {

result += i;

}

return result;

 

De compiler is niet slim genoeg om één StringBuilder te introduceren, omdat hij dat doet voor de + operator en die zit binnen de loop. De compiler zal dus het volgende genereren:

 

String result = “”;

for (int i = 0; i <= 100; i++) {

result = new StringBuilder(result)

.append(i)

.toString();

}

return result;

 

Een StringBuilder voor elke iteratie van de loop. Wat nogal pijnlijk is, is de performance. Een snellere oplossing zou zijn geweest om zelf de StringBuilder te introduceren:

 

StringBuilder result = new StringBuilder();

for (int i = 0; i <= 100; i++) {

result.append(i);

}

return result.toString();

 

{ For loops }

In Java 5 werd de ‘enhanced for loop’ geïntroduceerd. Dit is misschien wel de de facto standaard geworden om over een collectie heen te lopen.

 

for (String s : list) {

doStuffWith(s);

}

 

Als we naar de bytecode hiervan kijken staat er het volgende:

 

‘Iterator<String> iterator = list.iterator();

while (iterator.hasNext()) {

String s = iterator.next();

doStuffWith(s);

}

 

Er is dus niets enhanced aan de for loop, onderwater gebruik je nog steeds de iterator. Misschien heb je het soms wel eens gezien in een van de stack traces.

De paar voorbeelden die we hebben bekeken, zijn natuurlijk niet alle optimalisaties of dingen die veranderen tijdens compilatie. Wel geven ze een aardige indruk van wat de compiler en interpreter doen voor je op de achtergrond. De volgende keer gaan we kijken wat de Runtime te bieden heeft.

 

BIO

Jago de Vreede is Java developer bij OpenValue.