tifyty

pure Java, what else ?

Havi archívumok: július 2012

StringBuffer és StringBuilder

Az a cikk arról szól, hogy mennyivel gyorsabb a StringBuilder, mint a StringBuffer, és mikor melyiket érdemes használni. Seniorok számára nem lesz benne újdonság, talán a mérések eredménye hordoz némi tanulságot.

Amikor a Java-t tanuljuk, akkor szigorúan a fejünkbe verik, hogy a String az bizony immutable, és a string konkatenáció az ördögtől való, eszi a memóriát, másolgatja a stringet a memóriában ide-oda, új objektumokat hoz létre, és ez így igaz is. Amikor nem csak éppen két stringet akarunk konkatenálni egy Exception üzenet összerakására, amikor a program futásának már úgyis annyi, és ezért a teljesítmény sem különösebben érdekes akkor ott van a StringBuffer és a StringBuilder.

Mi különbség a kettő között?

A StringBuffer támogatja a multi-thread hívásokat, míg a StringBuilder nem. Ettől eltekintve teljesen kompatibilis a két osztály, api szinten ahol az egyik használható, ott használható a másik is. A metódusok azonban a StringBuffer-ben synchronize-áltak, és ez azt jelenti, hogy különböző szálak használhatnak biztonsággal közös buffert, de ugyanakkor minden metódushívás előtt és után a változók értékeinek szinkronizálása a memória és a processzor regiszterek és a processzor cache között megtörténik. Ez valamennyit lassít a futáson. Persze valamit, valamiért.


A StringBuffer a Java 1.0 verzióban már megjelent, míg a StringBuilder csak az 1.5-ben. Miért nem jelent meg rögtön az 1.0-ban mind a kettő, illetve miért a szinkronizált változat volt az első? Biztos válasz erre nincs, hiszen ez egy olyan döntés, amit a Java tervezői és fejlesztői hoztak, és ha csak valahol egy jegyzőkönyvben le nem írták, hogy miért döntöttek úgy, hogy a StringBuffer szinkronizált legyen, akkor valószínűleg már senki nem emlékszik rá.

De képzeljük magunkat egy pillanatra 1996. január 23. elé (ekkor jött ki az 1.0 Java), és tegyük fel a kérdést: legyen-e szinkronizált a tervezett StringBuffer minden metódusa? Mit is jelentett ekkor a szinkronizáltság? Azt amit ma? Nem egészen. Elvileg ugyan igen, de erősen máshol voltak a hangsúlyok. A szinkronizált metódusokba nem léphet be párhuzamosan két szál. Ez nem gond, mert a StringBuffer esetében ez baj is lenne. Akkor már inkább jobb, ha az egyik száll megáll, a másik meg vár, mintha egy rosszul megírt multi-thread program összekutyulja a buffert, aztán szidják a Java-t, hogy az szar. (Amúgy akkor még elég szar volt, de ez már a mellékvágány mellékvágánya lenne.) A másik dolog, hogy többprocesszoros rendszerekben a processzor cache és regiszterek szinkronizálását a memória felé el kell végezni, hogy ha a buffert egy másik processzoron futó szál piszkálta utoljára, akkor is jó állapottal tudjon a mi szálunk dolgozni. Ez akkoriban, amikor a 600MHz Alpha processzorok voltak a csúcstartók, és Linux még nyolc évre volt attól, hogy igazán elfogadhatóan fusson többprocesszoros gépen nem sok mindenkit érintett. A Java történet nem arról szólt, hogy mennyire jó, hanem arról, hogy életben marad-e egyáltalán.

Nézzük meg a gyakorlatban, hogy milyen sebesség különbségek lehetnek az egyes string kezelési megközelítések között. Vegyünk egy nagyon egyszerű példát: Vegyünk ki minden felesleges szóközt egy stringből. Más szavakkal, ahol egymás után több szóköz van, ott azokat cseréljük le egy szóközre.

Ez a feladat különösen kedves a szívemnek, mert először a BME-n jelent meg nyomtatásban a Z80 programozás jegyzetemben.

Az első megoldásban használjunk String-eket és reguláris kifejezéseket:

	public static String unspacerRegEx1(final String in) {
		String returnValue = in;
		Pattern pattern = Pattern.compile("^(.*)(\\s\\s)(.*)$");
		Matcher matcher = pattern.matcher(returnValue);
		while (matcher.find()) {
			returnValue = matcher.group(1) + " " + matcher.group(3);
			matcher = pattern.matcher(returnValue);
		}
		return returnValue;
	}

Ez a lehető legrosszabb megoldás, sem nem olvasható, sem nem hatékony. Már algoritmus szinten sem. Javítunk rajta, hogy ha nem két hanem több szóköz van egymás után, akkor azt egyszerre ismerje fel:

	public static String unspacerRegEx2(final String in) {
		String returnValue = in;
		Pattern pattern = Pattern.compile("^(.*)(\\s\\s+)(.*)$");
		Matcher matcher = pattern.matcher(returnValue);
		while (matcher.find()) {
			returnValue = matcher.group(1) + " " + matcher.group(3);
			matcher = pattern.matcher(returnValue);
		}
		return returnValue;
	}

Tovább javíthatunk a dolgon, ha nem rakjuk össze a részeredmény stringeket, hanem balról jobbra haladva érjük el az eredményt:

	public static String unspacerRegEx3(final String in) {
		String returnValue = "";
		Pattern pattern = Pattern.compile("(.*?)\\s\\s+");
		Matcher matcher = pattern.matcher(in);
		int end = 0;
		while (matcher.find()) {
			returnValue += matcher.group(1) + " ";
			end = matcher.end();
		}
		returnValue += in.substring(end);
		return returnValue;
	}

Persze továbbra is kétségeink lehetnek, hogy ez megfelelő megoldás-e, de azt várjuk, hogy lényeges sebességnövekedés érhető el, ha a string elég nagy. De még nagyobb hatékonyságnövekedést érhetünk el akkor ha megtaláljuk a reguláris kifejezésekhez használhatü replaceAll() metódust:

	public static String unspacerRegEx4(final String in) {
		Pattern pattern = Pattern.compile("\\s\\s+");
		Matcher matcher = pattern.matcher(in);
		String returnValue = matcher.replaceAll(" ");
		return returnValue;
	}

… és persze kísértést érezhetünk arra is, hogy a String osztályban definiált azonos nevű metódust használjuk:

	public static String unspacerString(final String in) {
		return in.replaceAll("\\s\\s+", " ");
	}

Ez nagyon egyszerű megoldás, tiszta, világos, olvasható. Általánosságban (és a mérési eredmények ismeretében) az is az *általános* javaslatom, hogy ezt a verziót érdemes használni és csak akkor keresni valami gyorsabbat, ha nem elég gyors a programunk és a profiler azt mondja, hogy itt van a palack nyaka. De ha ezt mondja, akkor megoldhatjuk a feladatot StringBuilder-rel is:

	public static String unspacerStringBuilder(final String in) {
		StringBuilder sb = new StringBuilder(in);
		boolean previousWasSpace = false;
		int j, i;
		for (i = 0, j = 0; i < sb.length(); i++) {
			sb.setCharAt(j, sb.charAt(i));
			boolean thisIsSpace = Character.isWhitespace(sb.charAt(j));
			if (!previousWasSpace || !thisIsSpace) {
				j++;
			}
			previousWasSpace = thisIsSpace;
		}
		sb.setLength(j);
		return sb.toString();
	}

Ez az algoritmus szépen pakolgatja a karaktereket, és ha két szóközt talál, akkor a második után nem lépteti a j target indexet, hanem rámásolja a következő karaktert (ami lehet szóköz, vagy nem szóköz, de ha szóköz, akkor megint csak nem lépteti).

Az unspacerStringBuffer kopizásától most itt eltekintünk, ugyanaz, mint a unspacerStringBuilder csak StringBuilder helyett StringBuffer-t használ. A fő program elején összeállít egy megfelelően (erre a szóra még visszatérünk) nagy stringet, és utána a parancssori argumentumnak megfelelően meghívja valamelyik metódust:

	public static void main(String[] args) {
		Long timeStart, timeEnd;
		final String testBase = "ap        le  pe                a          ch";
		final int multiplier = 1000;
		final StringBuilder sb = new StringBuilder(multiplier
				* testBase.length());
		for (int i = 0; i < multiplier; i++) {
			sb.append(testBase);
		}
		final String testString = sb.toString();
		final long loopSize = 5;
		switch (args[0]) {
		case "1":
			timeStart = System.currentTimeMillis();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				String z = unspacerRegEx1(testString);
			}
			timeEnd = System.currentTimeMillis();
			System.out.println(timeEnd - timeStart + "ms with unspacerRegEx1");
			break;
...
		}
	}
}

(A case többi ágától eltekinthetünk. A teljes program megtalálható itt becsukva:

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This class provides three different methods that remove the superfluous
 * spaces from a string.
 */
public class StringUnspacer {

	public static String unspacerRegEx1(final String in) {
		String returnValue = in;
		Pattern pattern = Pattern.compile("^(.*)(\\s\\s)(.*)$");
		Matcher matcher = pattern.matcher(returnValue);
		while (matcher.find()) {
			returnValue = matcher.group(1) + " " + matcher.group(3);
			matcher = pattern.matcher(returnValue);
		}
		return returnValue;
	}

	public static String unspacerRegEx2(final String in) {
		String returnValue = in;
		Pattern pattern = Pattern.compile("^(.*)(\\s\\s+)(.*)$");
		Matcher matcher = pattern.matcher(returnValue);
		while (matcher.find()) {
			returnValue = matcher.group(1) + " " + matcher.group(3);
			matcher = pattern.matcher(returnValue);
		}
		return returnValue;
	}

	public static String unspacerRegEx3(final String in) {
		String returnValue = "";
		Pattern pattern = Pattern.compile("(.*?)\\s\\s+");
		Matcher matcher = pattern.matcher(in);
		int end = 0;
		while (matcher.find()) {
			returnValue += matcher.group(1) + " ";
			end = matcher.end();
		}
		returnValue += in.substring(end);
		return returnValue;
	}

	public static String unspacerRegEx4(final String in) {
		Pattern pattern = Pattern.compile("\\s\\s+");
		Matcher matcher = pattern.matcher(in);
		String returnValue = matcher.replaceAll(" ");
		return returnValue;
	}

	public static String unspacerString(final String in) {
		return in.replaceAll("\\s\\s+", " ");
	}

	public static String unspacerStringBuilder(final String in) {
		StringBuilder sb = new StringBuilder(in);
		boolean previousWasSpace = false;
		int j, i;
		for (i = 0, j = 0; i < sb.length(); i++) {
			sb.setCharAt(j, sb.charAt(i));
			boolean thisIsSpace = Character.isWhitespace(sb.charAt(j));
			if (!previousWasSpace || !thisIsSpace) {
				j++;
			}
			previousWasSpace = thisIsSpace;
		}
		sb.setLength(j);
		return sb.toString();
	}

	public static String unspacerStringBuffer(final String in) {
		StringBuffer sb = new StringBuffer(in);
		boolean previousWasSpace = false;
		int j, i;
		for (i = 0, j = 0; i < sb.length(); i++) {
			sb.setCharAt(j, sb.charAt(i));
			boolean thisIsSpace = Character.isWhitespace(sb.charAt(j));
			if (!previousWasSpace || !thisIsSpace) {
				j++;
			}
			previousWasSpace = thisIsSpace;
		}
		sb.setLength(j);
		return sb.toString();
	}

	public static void main(String[] args) {
		Long timeStart, timeEnd;
		final String testBase = "ap        le  pe                a          ch";
		final int multiplier = 1000;
		final StringBuilder sb = new StringBuilder(multiplier
				* testBase.length());
		for (int i = 0; i < multiplier; i++) {
			sb.append(testBase);
		}
		final String testString = sb.toString();
		final long loopSize = 5;
		switch (args[0]) {
		case "1":
			timeStart = System.currentTimeMillis();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				String z = unspacerRegEx1(testString);
			}
			timeEnd = System.currentTimeMillis();
			System.out.println(timeEnd - timeStart + "ms with unspacerRegEx1");
			break;
		//
		// ------------------------------------------------------------------------
		//
		case "2":
			timeStart = System.currentTimeMillis();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				String z = unspacerRegEx2(testString);
			}
			timeEnd = System.currentTimeMillis();
			System.out.println(timeEnd - timeStart + "ms with unspacerRegEx2");
			break;
		//
		// ------------------------------------------------------------------------
		//
		case "3":
			timeStart = System.currentTimeMillis();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				String z = unspacerRegEx3(testString);
			}
			timeEnd = System.currentTimeMillis();
			System.out.println(timeEnd - timeStart + "ms with unspacerRegEx3");
			break;
		//
		// ------------------------------------------------------------------------
		//
		case "4":
			timeStart = System.currentTimeMillis();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				String z = unspacerRegEx4(testString);
			}
			timeEnd = System.currentTimeMillis();
			System.out.println(timeEnd - timeStart + "ms with unspacerRegEx4");
			break;
		//
		// ------------------------------------------------------------------------
		//
		case "5":
			timeStart = System.currentTimeMillis();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				String z = unspacerString(testString);
			}
			timeEnd = System.currentTimeMillis();
			System.out.println(timeEnd - timeStart + "ms with unspacerString");
			break;
		//
		// ------------------------------------------------------------------------
		//
		case "6":
			timeStart = System.currentTimeMillis();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				String z = unspacerStringBuilder(testString);
			}
			timeEnd = System.currentTimeMillis();
			System.out.println(timeEnd - timeStart
					+ "ms with unspacerStringBuilder");
			break;
		//
		// ------------------------------------------------------------------------
		//
		case "7":
			timeStart = System.currentTimeMillis();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				String z = unspacerStringBuffer(testString);
			}
			timeEnd = System.currentTimeMillis();
			System.out.println(timeEnd - timeStart
					+ "ms with unspacerStringBuffer");
			break;
		}
	}
}

)

A mérési eredmények:

verhasp:string-test verhasp$ sh target/classes/execute.sh 
112508ms with unspacerRegEx1
110590ms with unspacerRegEx2
846ms with unspacerRegEx3
105ms with unspacerRegEx4
92ms with unspacerString
30ms with unspacerStringBuilder
42ms with unspacerStringBuffer

Értékelés: látszik, hogy a StringBuilder valóban a leggyorsabb, de a String.replaceAll() sem sokkal lassabb. Igaz, hogy a háromszorosa, de mi ez a leglassabb megoldás 3000-szeres futási idejéhez képest.

Megjegyzések: a sebességet nagyon sok minden befolyásolja. Ha egy JVM-ben futtattam egymás után a teszteket, akkor előfordult, hogy az egyik metódus lefutott, elindult a második, és beütött egy GC. Ezt valamennyire kivédte, ha minden metódus hívás előtt hívott a program egy System.gc()-t de ilyenkor is sok minden befolyásolta az egyes futási időket. Külön processzként elindítva egyértelmű és világos eredményt kaptunk. Az azonban, hogy az első próbálkozások során például a unspacerRegex4 rendszeresen gyorsabb volt, mint az unspacerString, illetve kisebb méretű stringekre a StringBuilder-t használó megoldás sokkal lassabb volt, mint a unspacerRegex4 vagy unspacerString azt mutatja, hogy optimalizálni csak a tényleges futó kódot lehet, mérésekkel. Premature optimization is source of evil.

Irodalom:

String immutable reloaded

A korábbi A String immutable, vagy nem? cikk meglehetősen rövid volt. Nem titkolt (bár közzé sem tett) szándékom az volt, hogy az olvasókat gondolkodásra ösztönözzem, és azt hiszem, hogy a visszajelzések alapján (ami személyes levélben is jócskán jött) ez meg is történt. Azonban voltak olyan visszajelzések is, amelyek szememre vetették, hogy nem szabad ilyen hekkeléseket csinálni, holott a cikk nem akarta azt sugallni, hogy a String osztály a Java nyelvben ilyen módon is használandó lehet.

Ezért most “újra” megírom ezt a cikket. Először elmondom, hogy mit is csinál ez a program, majd pedig azt, hogy miért is érdemes ezt tudni, melyek azok az esetek, amikor szükségünk lehet erre az ismeretre. És akkor, mivel elég rövid, hogy ne kelljen lapozgatni, az eredeti cikk mégegyszer:


Mit ír ki a következő program?

package com.verhas.elrettentopelda;

import java.lang.reflect.Field;

public class Wtf {
	public static void main(String[] args) throws Exception {
		String alma = &quot;alma&quot;;
		String korte = &quot;alma&quot;;
		Field value = String.class.getDeclaredField(&quot;value&quot;);
		value.setAccessible(true);
		value.set(alma, new char[] { 'k', 'o', 'r', 't', 'e' });
		System.out.println(korte);
	}
}

Mit csinál ez a program

Először is tisztázzuk, hogy a program olyan dolgot követ el, ami ugyan a gyakorlatban működik, de semmi nem garantálja, hogy valóban működőképes mindig, minden Java környezetben. Ugyanis nem csak a String osztály definiált, és garantált működését használja ki, hanem reflection használatával olyan mezőkhöz nyúl, amelyek ugyan szerepelnek az implementációban, ám minden előzetes figyelmeztetés nélkül a fejlesztők megváltoztathatják ezeket. Nem azért vannak, hogy piszkáljuk, de senki nem akadályoz meg bennünket abban, hogy megtegyük.

UPDATE(2013-11-08): Java8-ban meg is tették a fejlesztők, a fenti példa nem működik 8-as Java alatt!

Tehát ha valaki ilyesmire vetemedik, akkor az megérdemli. Az nem Java-ban programoz, hanem csak kódot állít elő Java nyelven.

A program két String tíusú változót hoz létre. A Java fordító észreveszi, hogy ez a két string literál azonos, ezért a lefordított kódban valóban azonos lesz a két string, annyira, hogy csak egyszer fog szerepelni a string literálok között. Azaz alma==korte. Ezt követően reflection használatával elkérjük a String osztály value nevű mezőjét. Vigyázat, kezdők, és akik nem jártasak a reflection-ben: NEM a value mező értékét kérjük el, már csak azért sem, mert nem egy String típusú objektum value mezőjéről beszélünk, hanem az osztály value mezőjéről. Ez nem egy static mező, így értéke sincs, nem osztálypéldány. Ettől függetlenül még kaphatunk egy Field típusú objektumot, amelyik metódusai ezen a mezőn végeznek műveletet.

Amikor megvan ez az objektumunk, akkor a hozzáférést true értékre állítjuk. Ezzel elérjük, hogy azokat a mezőket is el tudjuk érni, amelyek private módosítóval rendelkeznek, vagy egyéb okok miatt nem férhetnénk hozzá a mezőhöz (pl. protected a mező, és nem azonos csomagban vagyunk, sem pedig leszármazottak nem vagyunk). Innen kezdve a field objektumon keresztül el tudjuk érni egy String objektum value mezőjét.

Ezt követően ezt a mezőt átállítjuk. Az értéke egy karakter tömb lesz, a k, o, r, t, e értékekkel, és a hossza 5. Ez azonban nem a string hossza, csak a string objektum value mezőjének a hossza. A String karakterei ugyanis csupán valahol ebben a tömbben vannak, nem feltétlenül az első elemnél kezdődik a string és nem feltétlenül tart a tömb végéig. A string objektum hosszát nem állítottuk át, az maradt továbbra is a "alma".length() értéke, ami 4.

Ennek folyományaként pedig a a string objektum értéke "kort", mert a hossz érték 4.

Érdekes, hogy a szavazás állása szerint jelen pillanatban (2012-07-26 11:30pm) a korte 12:8-ra vezet a helyes kort-tel szemben, de hát a tények makacs dolgok, ez nem demokrácia, hogy a szavazás döntsön.

Miért érdemes ezt tudni?

Például érdemes tudni, hogy a substring metódus nem fogja a karaktereket átmásolni, hanem az eredeti és a substring által visszaadott string ugyanazt a karakter tömböt fogja használni. Ezért nem különösebben nagy költség, még igen nagy string objektumoktól sem elkérni substring-et. (Stringet konkatenálni persze az már más.)

Azért is érdekes lehet a kérdés, hogy tisztában legyünk azzal, hogy ha kiadunk egy JAR fájlt a kezünkből, akár obfuscated lebutítva, hogy ne nagyon lehessen visszafejteni, és elektronikusan aláírva, hogy ha valaki belepiszkál, akkor a class loader már ne töltse be (melyik class loader?) azzal nagyon sokat nem növeltünk a biztonságon.

Készítettünk licence ellenőrző modult, most is elérhető LGPL License3j néven. Ez abban segíti a felhasználót, hogy ne használja véletlenül a szoftvert olyan esetben, amikor nincs rá joga. De arra izmozni, hogy ne is tudja, nem érdemes, mert azt még C-ben sem lehet. A natív gépi kódot is át lehet írni, a JVM byte kódba is bele lehet piszkálni, lehet class loadert írni, ami kikerüli a betöltési védelmeket, és végül, de nem utolsó sorban át lehet írni reflection-nel a futó Java-ban bármit.

De aki ilyet tesz, az hacker. Nem programozó.

Ez már tényleg WTF!!!

Ha valakinek sikerült megtartani a lelki békéét a korábbi A string immutable vagy nem elolvasása után, és nem kezdte el mondogatni, hogy a “Java egy szar” vagy valami hasonlót, akkor azt ajánlom, hogy olvasd el ezt a szösszenetet, és utána rohanj végig a folyosón üvöltve, hogy elmégy Nepálba kecskepásztornak.

import java.lang.reflect.Field;

public class IntHack {
	public static void main(String[] args) throws Exception {
		Integer a1 = 1;
		Integer a2 = 2;
		Integer a3 = 3;
		Integer a4 = 4;
		Integer a5 = 5;
		Integer a128 = 128;
		Field f = Integer.class.getDeclaredField("value");
		f.setAccessible(true);
		f.setInt(a1, 666);
		f.setInt(a2, 666);
		f.setInt(a3, 666);
		f.setInt(a4, 666);
		f.setInt(a5, 666);
		f.setInt(a128, 666);
		Integer b1 = 1;
		Integer b2 = 2;
		Integer b3 = 3;
		Integer b4 = 4;
		Integer b5 = 5;
		Integer b128 = 128;
		System.out.println(b1);
		System.out.println(b2);
		System.out.println(b3);
		System.out.println(b4);
		System.out.println(b5);
		System.out.println(b128);
	}
}

És mit ír ez ki?

666
666
666
666
666
128

Nagyon gonosz! Nagyon, nagyon gonosz!

No akkor, ha kifutottad magad a folyosókon, és rájöttél, hogy a nepáli kecskepásztorok élete sem olyan egyszerű, mert bár ott nincs reflection, de még tükör sem, viszont ott van … Mi van ott? Nincs ott semmi. Na hagyjuk. Szóval, akkor nézzük meg, hogy mi a búbánatos ecske történt itt.

Létrehoztunk néhány Integer változót, és értéket adtunk nekik. Majd elővettük a gonosz kelléktárát, és megerőszakolva a Java lelkivilágát, átírtuk az alapvetően immutable Integer objektumok értékeit. Azért igaza van annak, hogy aki ilyent tesz, az meg is érdemli, de ifjonti hévvel akár az is gondolhatjuk, hogy ha lehet, akkor miért ne?

Hogy miért? Azért, mert ha olyasmit csinálunk, ami nincs benne a nyelvi kontraktusban, csak éppen meg lehet csinálni, akkor bármi egyéb olyan dolog is történhet, amire nem számítunk. Mert ha egyszer a Java runtime azt mondja, hogy az Integer immutable, tehát nem változtatható meg a szokásos nyelvi elemekkel, akkor bizony a futtató környezet erre számít is. Az, hogy éppen van egy olyan eszköz, ami alkalmas arra, hogy megszegjük ezt a kontraktust, és mégis megváltoztassuk a megváltoztathatatlant nem kell, hogy érdekelje a Java-t: ez a mi felelősségünk. A villamosművek sem felelős azért, ha valaki bele \pisil a konnektorba.

Amit a JVM pedig tesz, bízva abban, hogy az Integer immutable, az az, hogy létrehoz előre 256 darab Integer objektumot -128 és 127 közötti értékekkel, beleértve a határokat is. Amikor pedig ebbe az intervallumba eső int értéket talál a Java kódban, akkor nem hoz létre új objektumot az autoboxinghoz, hanem felhasználja ezeket a már létrehozott objektumokat. Ennek pedig az is a következménye, amit a mellékelt kód is demonstrál, hogy ha átírjuk reflection-nel ezeknek az objektumoknak az értékeit, akkor az egész JVM-ben felrúghatjuk a -128 és 127 közötti Integer konstans objektumok értékeit.

VIGYÁZAT: azért nem az int literál konstansok értéket írjuk felül, csak az autoboxinghoz használt “final” Integer objektumok értékeit. Az igazi, genuine int változóink továbbra is megmaradnak, és a helyes értéket adják. És ha nem autoboxinggal hozunk létre Integer objektumot, akkor az is korrekt lesz. Ezt demonstrálja a következő kis kódrészlet:

	public static void main(final String[] args) throws Exception {
		Integer a;
		Integer b;
		a = 1;
		final Field f = Integer.class.getDeclaredField("value");
		f.setAccessible(true);
		f.setInt(a, 2);
		b = 1;
		System.out.println(a == b);
		System.out.println(b);
		a = new Integer(1);
		b = new Integer(1);
		System.out.println(a == b);
		System.out.println(b);
	}

Ennek eredménye:

true
2
false
1

De miért csinálja ezt a JVM? Azért, mert ezek azok az értékek, amelyek int konstansként a leggyakrabban előfordulnak a kódjaink futása során, és ha nem kell minden egyes alkalommal új Integer objektumot létrehoznia, akkor az valamennyit gyorsítani fog a kódon. Hogy mennyit is?

public class IntSpeed {

	private final static int N = 1000_000_000;

	private static void autoboxing() {
		for (int i = 0; i < N; i++) {
			Integer a = i % 128;
		}
	}

	private static void noboxing() {
		for (int i = 0; i < N; i++) {
			Integer a = new Integer(i % 128);
		}

	}

	public static void main(String[] args) {

		long tStart = System.currentTimeMillis();
		autoboxing();
		long tEnd = System.currentTimeMillis();
		System.out.println(tEnd - tStart);
		tStart = System.currentTimeMillis();
		noboxing();
		tEnd = System.currentTimeMillis();
		System.out.println(tEnd - tStart);
	}
}

Ezt Eclipse-ben debug módban futtatva (hogy a JIT ne zavarjon, és ne optimalizálja ki a nem használt változóinkat) 12ms és 7000ms futási időt produkált. Ha nem debug módban futtatjuk, akkor azért az sokat segít, de így is lényeges a különbség: 10ms és 40ms a futási idő.

És miért érdekes ez az egész történet? Miért kell ezzel foglalkozni? Egyszerűen nem csinálunk ilyent, és nem fáj a fejünk, nem futunk bele ilyen szituációba.

És az innen onnan összeszedett köcsögök (JAR-ok) ?

Egy kis segédosztály debuggoláshoz

A debugger az debugger, megkönnyíti az életet, de az igazi hardcore programozó még mindig nem használ debuggert, csak loggol. Amúgy meg, esélyed sincs debuggolni éles környezetben, a fejlesztő és teszt környezetben pedig nem lehet a hibákat reprodukálni. Hogy miért nem lehet, azt senki sem tudja, de a tapasztalat az, hogy nem lehet. Ahogy az oroszok mondják, ha tudnak franciául: Act Of God.

Szóval nyomatjuk a logokat, mert mi hardcore-ok vagyunk, de persze kényelmesek is vagyunk, … Hol van már az az idő, amikor FORTRAN-ban… (Nem! Nem potencia növelő reklám követezik!) Szóval szeretnénk valamit ami loggol, és mondjuk loggolja, hogy merre járunk.

Mivel loggolunk? Na ez is megér egy misét, de most ebbe ne menjünk bele. Maradjunk abban, hogy SLF4J és mögötte, vízkereszt, vagy amit akartok. Ez a legjobb választás ma. Tegnap lehet, hogy más volt, és lehet, hogy holnap nem ez lesz a legjobb választás, de ma ez.

Sokszor csak arra vagyunk kíváncsiak, hogy merre jár a program. Teletűzdelhetjük mindenféle loggoló parancsokkal, amik ki is írják általában, hogy merre járunk, de kényelmesebb lenne ha csak annyit kellene írni, hogy Tracer.trace().

Ehhez készítettem magamnak egyszer (most meg újra) egy kis köcsögöt (gy.k.: JAR-t). Van benne egy interface, mert azért azt úgy illik Tracer meg egy gyár is (factory) TracerFactory, és néhány implementáció: NullTracer (ez nem csinál semmit) valamint SimpleTracer.

Az inteface nagyon egyszerű:

package tifyty.utility.debugtracer;
public interface Tracer {
	public abstract void trace();
	public abstract void trace(final String name, Object ... objects);
}

és az egyszerű implementáció is:

package tifyty.utility.debugtracer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SimpleTracer implements Tracer {
	private final Logger LOG;

	protected SimpleTracer(final String name) {
		LOG = LoggerFactory.getLogger(name);
	}

	public void trace() {
		trace(null);
	}

	public void trace(final String name, final Object... parameters) {
		final StackTraceElement[] ste = Thread.currentThread().getStackTrace();
		final StackTraceElement caller = ste[name == null ? 3 : 2];
		LOG.info("{}:{}/{}.{}({})",
				new Object[] { caller.getFileName(), caller.getLineNumber(),
						caller.getClassName(), caller.getMethodName(), name });
	}
}

A null implementáció meg aztán a legegyszerűbb, mint egy fakecske:

package tifyty.utility.debugtracer;
public class NullTracer implements Tracer {
	public void trace() {
	}
	public void trace(final String name, Object ... objects) {
	}
}

NOTE: A commentekben ne vitázzunk a loggolókról. Ha kell, szavazzatok arra, hogy kivesézzem azt a témát, és akkor majd ott. És akkor az elmaradhatatlan szavazó:

A feltételes operátor c ? a : b

Mostanában ért egy kis meglepetés, mert a lefordított Java nem úgy működött, ahogyan vártam. Ez persze gyakran megesik ebben a szakmában: ilyenkor kezdődik a debuggolás. Most is ez volt, de ahelyett, hogy a saját kódomban találtam volna hibát, valahol máshol találtam. Nem jó kódot generált a javac fordító. (Gondoltam én.)

Az osztály, amelyik demonstrálja a gondot a következő:

public class Bug{
    public static Number q(Number in) {
        return in instanceof Long ? 1L : 1.0;
    }
}

Egyszerűen csak annyit kellene tennie, hogy egy Long vagy egy Double típusú 1 értéket ad vissza, attól függően, hogy az argumentum Long vagy Double típusú. A kód azonban minden esetben Double típust ad vissza, és ez nem jó.

Megnéztem, hogy milyen kódot generál a javac. Javap disassemblerrel visszafordítva láthatjuk, hogy (ugyan elég bután, hiszen a javac nem optimalizál) valóban ez történik:

public static java.lang.Number q(java.lang.Number);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0       
         1: instanceof    #2                  // class java/lang/Long
         4: ifeq          11
         7: dconst_1      
         8: goto          12
        11: dconst_1      
        12: invokestatic  #3                  // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
        15: areturn       

Akár Long instance az argumentum változó, akár nem, mindenképpen double típusú konstans 1 értéket tölt az operatív stack-re a bytecode (dconst_1). És ez akkor is így van, ha bármilyen kifejezést írok a kérdőjel elé, és akkor is ha megfordítom a kettőspont előtt és után levő értékeket. Ez a kód így bizony nem jó. Mégsem a javac-ban van a hiba, mert a generált kód ha nem is jó (nekem) mégis pontosan az, aminek lennie kell a Java szabvány definíciója szerint.

(Akkor hol a hiba? Hát a fejemben!)

A szabvány, ami elérhető a http://docs.oracle.com/javase/specs/jls/se7/html/index.html oldalról azt mondja a ternary operátorról, hogy

Otherwise, if the second and third operands have types that are convertible (§5.1.8) to numeric types, then there are several cases:

Otherwise, binary numeric promotion (§5.6.2) is applied to the operand types, and the type of the conditional expression is the promoted type of the second and third operands.

Ami annyit tesz, hogy ha a második és harmadik operandus numerikus értékké konvertálható, akkor sok eset lehet, és ezek között a mi esetünkre vonatkozó szerint “numeric promotion” fog történni az operandusokkal, a §5.6.2-nak megfelelően és az egész feltételes kifejezés típusa is ennek megfelelő lesz.

Az §5.6.2 paragrafus pedig azt mondja, hogy

If either operand is of type double, the other is converted to double.

Vagyis, ha az egyik operandus double akkor a másik is double típusúvá lesz konvertálva.

A String immutable, vagy nem?

Mit ír ki a következő program?

package com.verhas.elrettentopelda;

import java.lang.reflect.Field;

public class Wtf {
	public static void main(String[] args) throws Exception {
		String alma = "alma";
		String korte = "alma";
		Field value = String.class.getDeclaredField("value");
		value.setAccessible(true);
		value.set(alma, new char[] { 'k', 'o', 'r', 't', 'e' });
		System.out.println(korte);
	}
}

Synchronized metódus, vagy synchronized blokk?

Nézzünk egy nagyon egyszerű Java osztályt, ami fel van megfelelően készítve arra, hogy több szálon is futtathatják ugyanazt a példányt:

public class SynchronizedClass {
	public synchronized void a() {
	}
	public void b() {
		synchronized (this) {
		}
	}
}

Fordítsuk le, és nézzük meg a b() illetve az a() metódusokból generált kódot, és borzadjunk!

verhasp:java verhasp$ javac SynchronizedClass.java 
verhasp:java verhasp$ javap -v -p SynchronizedClass.class 
Classfile /Users/verhasp/epam/.../src/main/java/SynchronizedClass.class
  Last modified 2012.07.08.; size 406 bytes
  MD5 checksum 46a80e90927a503c1838a55ffb5e154e
  Compiled from "SynchronizedClass.java"
public class SynchronizedClass
  SourceFile: "SynchronizedClass.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:

 ... ezt itt kitöröltem, mert csak zavar ...

{
 ... itt van a default konstruktor leírása, meg kódja, semmi extra, törölve ...

  public synchronized void a();
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return        

  public void b();
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0       
         1: dup           
         2: astore_1      
         3: monitorenter  
         4: aload_1       
         5: monitorexit   
         6: goto          14
         9: astore_2      
        10: aload_1       
        11: monitorexit   
        12: aload_2       
        13: athrow        
        14: return        
      Exception table:
         from    to  target type
             4     6     9   any
             9    12     9   any

      StackMapTable: number_of_entries = 2
           frame_type = 255 /* full_frame */
          offset_delta = 9
          locals = [ class SynchronizedClass, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
           frame_type = 250 /* chop */
          offset_delta = 4

}
verhasp:java verhasp$ 

Az a() metódus csak egy return. Nem is várunk mást tőle. Persze a modifier-ek között ott van, hogy synchronized, és ezt a JVM figyelembe is veszi. Ellenben a b() metódus. Na az nem egy matyóhímzés! (Elnézést kérek azoktól, akik szerint a matyóhímzés ronda, és ezzel most megbántottam őket, feltéve, hogy ők meg elnézést kérnek a matyó hímzőktől.)

A b() metódus kódja felhasznál két lokális változót, operátor vermet két mélységben. Van egy exception táblája, és külön gondoskodik arról, hogy ha bármilyen kivétel dobódna a szinkronizált részben, akkor az el legyen kapva, akkor is kilépjen a szinkronizációból a kód, és utána újra legyen dobva a kivétel. Ennél már az is jobb, ha a szinkronizálandó blokkot kiemeljük egy külön metódusba:

	public void b() {
		a();
	}

Aminek a byte kódja:

  public void b();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: invokevirtual #2                  // Method a:()V
         4: return       

Hát nem egyszerűbb?

Ez jusson eszedbe valahányszor szinkronizált blokkot akarsz írni.

És a JIT optimalizál? De még mennyire!

Egy korábbi postban volt szó róla, hogy a javac nem optimalizál. Most nézzünk meg, hogy a JIT optimalizál-e. A JIT-ről elvileg tudjuk, hogy optimalizál, sőt nem csak a hagyományos értelemben teszi, de futás közben a futási eredmények alapján is néha újrafordítja a kódot. De vajon mennyire látszik ez meg? Van egy nagyon egyszerű példa, ami megmutatja, hogy futási időben is mennyire meg tud változni egy adott kódrészlethez tartozó futási idő. Ebben a cikkben ezt fogom megmutatni. Mielőtt belemennénk, a következőt kérem tőled. Olvasd végig a posztot, és ne hülyézz le az elején, közepén, illetve/és ne maradj tudatlan mert nem olvasod végig. Ugyanis csalok, de csak egy kicsit. A nagy “durranás” a vége felé jön. Persze ha tudod, akkor nem olyan nagy durranás.

Spoiler alert. A következő text blokkban leírom, hogy mi a csalás, ha meg akarsz lepődni, akkor ne nyisd ki.

A két sebesség közötti különbség, ami a példákban látható nem a JIT hatása,
vagy nem csak a JIT hatása. A processzor maga is optimalizál, és ezt a
C kód bizonyítja, hiszen ott szó nincsen JIT-ről. 

Ha kinyitottad, akkor is van a cikk végén egy extra meglepetésem a számodra.

A következő kis metódust futtassuk:

	public static double summUp(double[] a) {
		double s = 0.0;
		for (double d : a) {
			if (d > 0.5)
				s += d;
		}
		return s;
	}

Ez annyit tesz, hogy a double[] a azon elemeit, amelyek nagyobbak, mint 0.5 összeadja. Elég egyszerű, mint a fék fapofa. A meghíváshoz írjunk egy kis programot, legyen a metódus neve main és public static a szokásos módon.

	public static void main(String[] args) {
		double[] a = new double[100_000_000];
		Random r = new Random();
		for (int i = 0; i < a.length; i++) {
			a[i] = r.nextDouble();
		}
		long tStart = System.currentTimeMillis();
		double s = summUp(a);
		long tEnd = System.currentTimeMillis();
		System.out.println(s);
		System.out.println(tEnd - tStart);
		Arrays.sort(a);
		tStart = System.currentTimeMillis();
		s = summUp(a);
		tEnd = System.currentTimeMillis();
		System.out.println(s);
		System.out.println(tEnd - tStart);
	}

Egy aprócska, alig százmillió elemet tartalmazó tömböt hozunk létre, feltöltjük 0.0 és 1.0 közötti értékekkel, és erre hívjuk meg a metódusunkat. Kétszer. Azért persze, hogy ne legyen olyan unalmas az élet közben sorba is rendezzük a tömböt, és kiíratjuk, hogy mennyi ideig futott az összegző program. Az eredmény pedig meglepő:

verhasp:java verhasp$ javac SortedNotSorted.java
verhasp:java verhasp$ java SortedNotSorted
3.750628033806522E7
537
3.750628033807129E7
173
verhasp:java verhasp$

Az összeget csak ellenőrzésképpen írjuk ki, hogy

  1. lássuk, hogy az összeadás sorrendjétől független eredményt kapunk, nincs valami vad hiba a programban
  2. lássuk, hogy double aritmetikával soha nem kapjuk ugyanazt az eredményt, de azért elég jó

Ami igazán érdekel bennünket, az a futási idő ms-ban. A rendezetlen tömb esetében az összegzés 537ms, majdnem fél másodperc (azért csak 50millió számot adtunk össze), míg rendezett esetben ennek kb. a fele. Mi történt itt? A JIT rájött, hogy rendezett a tömb, és csak a második felére futtatja a ciklust? Azért ez egy kicsit húzós lenne, ennyire azért nem jó az optimalizáció. De valami igazság mégiscsak van a dologban.

A futás során az if utasítás kiértékelése előre megtörténik, és amikor a tömb rendezett, akkor sokkal jobb eséllyel találja el a környezet, hogy mi lesz az eredmény, és ennek megfelelően a megfelelő ág futását elő tudja készíteni. Például olyan kódokat tölt be a processzor cache-be, amelyek annak az ágnak a futtatásához kellenek.

És itt vége is lehetne a cikknek, levontuk a konklúziót: a JIT bizony optimalizál, mégpedig igen jól, és okosan. De tényleg csak a JIT-től lett ilyen?

A futás során nem csak a JIT optimalizál, hanem a processzor is. ( ??? ) Hát bizony de. Ha fiatal vagy, és még nem tanultál ilyent, akkor most egy kicsit olvashatsz ilyesmiről, ha meg olyan öreg vagy, mint én, aki még z80-on kezdte az assembly-t és mégis meglepődsz, akkor nem haladtál a korral, csak öregedtél.

A processzorok szinte mindent cache-ből szeretnek csinálni. A Z80-hoz képest (4MHz) olyan sebességnövekedést értek el, hogy ma már a memória háttértárnak tekinthető. Olyan, mint régen a diszk. Ha a memóriához kell nyúlnia a processzornak, akkor az bizony lassú lesz. Ezért is kerüld a synchronized metódusokat és a volatile változókat ameddig csak lehet (miért is?). Ha pedig mégis a memóriához kell nyúlni, akkor a processzor megpróbálja beolvasni az adatokat előre. Megpróbálja kitalálni, hogy az elágazási feltételnek mi lesz az eredménye, és a találgatásnak megfelelően kezdi el betölteni, és néha végre is hajtani a kódot (legfeljebb eldobja az eredményt, amit közben kiszámolt, ha tévedett), és mire oda jut, hogy megvan az igazi eredmény, addigra a betöltés, végrehajtás már előre jár. Persze mindig csak annyit tud előre menni, amennyi a regiszterekbe, a cache-be befér, és amennyi számolás nem függ a még el nem végzett számolások eredményétől.

Hogyan tudnánk eldönteni, hogy a JIT-e az igazi nagyágyú, vagy a processzor? Írjuk meg ugyanezt a kódot assembly-ben, és nézzük meg annak a futását!

Na én se hülyülök már meg, hogy ilyeneket találok ki magamnak, de végső soron ez lenne az igazi. Most azonban elégedjünk meg azzal, hogy írjuk meg a kódot C-ben. Ma már a C legalább olyan, mint régen az assembly volt, csak a poros fiókból előhúzott öreg szakik ismerik, akik felváltva zsonglőrködnek FORTRAN, C, COBOL és RPG programokkal. De legyen, és íme:

#include <stdio.h>
#include <stdlib.h>

#define LENGTH 100000000

static double summUp(double *a){
  double s = 0.0;
  int i;
  for( i = 0; i < LENGTH ; i++ ){
      if( a[i] > 0.5 )
        s+=a[i];   
    }
  return s; 
}

long currentTimeMillis(){
  struct timeval tv;
  gettimeofday(&tv,NULL);
  return (tv.tv_sec*1000) + (tv.tv_usec/1000);
  }
  
int compareDouble(const void *x, const void *y) {
  return (*(double*)x > *(double*)y) ? 1 : -1;
}

int main(int argc, char *argv[]){
  double *a = (double *)malloc(LENGTH*sizeof(double));
  int i;
  for( i = 0 ; i < LENGTH ; i++ ){
    a[i] = rand()/((double)RAND_MAX + 1); 
    }
  long lStart = currentTimeMillis();
  double s = summUp(a);
  long lEnd =currentTimeMillis();
  printf("%lf\n%ld\n",s,lEnd-lStart);
  qsort(a,LENGTH,sizeof(double),compareDouble);
  lStart = currentTimeMillis();
  s = summUp(a);
  lEnd =currentTimeMillis();
  printf("%lf\n%ld\n",s,lEnd-lStart);
}

(Dobod el gyorsan! Nem szégyelled magad! C-ben programozni! Ki hallott ilyent!)

Ha már sikerült kiizzadni, futtassuk is le:

verhasp:java verhasp$ gcc SortedNotSorted.c
verhasp:java verhasp$ ./a.out 
37502647.627436
875
37502647.627257
428

És íme, láthatjuk, hogy a gyorsulás a C-ben megírt program esetében is tetten érhető. Ez tehát nem a JIT, hanem a processzor. Azaz… Várjunk csak? Hogy voltak ezek a millisecundumok? Gyorsabb a Java, mint a C?



Ez a cikk a Why is processing a sorted array faster than an unsorted array? cikk alapján készült, és nem sokat tesz ahhoz hozzá. (Eufemizmus, valójában semmit sem tesz hozzá.) Annyira meglepett amikor olvastam, hogy magam is ki akartam próbálni, és ha már megtettem, és kiizzadtam, a Java mellett a C verziót is, akkor már csak leírtam itt is, hogy aki nehezebben emészti az angol szövegeket, az is. Végül is megérdemlitek.

UPDATE

leningrad commentjei alapján elvégeztem a következő mérést:

verhasp:java verhasp$ sed s/i\+\+/\+\+i/ < SortedNotSorted.c > SortedNotSorted1.c
verhasp:java verhasp$ gcc -o SortedNotSorted1 SortedNotSorted1.c 
verhasp:java verhasp$ ./SortedNotSorted1 
37502647.627436
929
37502647.627257
469
verhasp:java verhasp$ gcc SortedNotSorted.c
verhasp:java verhasp$ ./a.out 
37502647.627436
889
37502647.627257
591

>>>Magyar László mint “poros fiókból előhúzott öreg szaki” magyarázatot kérek, hogy a C miért volt lassabb, mint a java??? csak mert nem volt kioptimalizálva a fordítás???<<<

verhasp:java verhasp$ gcc -O3 SortedNotSorted.c
verhasp:java verhasp$ ./a.out 
37502647.627436
601
37502647.627257
293

Sokat segít a -O3, de nem eleget. A Java/JIT páros még így is gyorsabb.

Hülyehiba

Egy másik cikk kapcsán készült, nem végleges kód:

                double[] a = new double[100_000_000];
		Random r = new Random();
		for (double d : a) {
			d = r.nextDouble();
		}

Már az is gyanús volt, hogy az Eclipse warningot jelzett a sorra, de hogy működni sem akart…

A szavazódoboz csak most, speciálisan többszörös szavazást is megenged.


UPDATE az első két komment után: A d változó nem ciklusszámláló. A következő program működik.

import java.util.Random;

public class TestLoop {
	public static void main(final String[] args) {
		class rndDouble {
			double d;
		}
		final rndDouble[] a = new rndDouble[100];
		for (int i = a.length - 1; i >= 0; i--) {
			a[i] = new rndDouble();
		}
		final Random r = new Random();
		for (final rndDouble d : a) {
			d.d = r.nextDouble();
		}
		for (final rndDouble d : a) {
			System.out.println(d.d);
		}
	}
}

persze sokat nem csinál, de legalább azt igen.

A Javac optimalizál? Nem úgy tűnik…

Általában azt szoktuk Java körökben mondogatni, hogy elsősorban szép kódot kell írni, a többit majd megoldja a fordító. Például ha egy logikai kifejezés olyan bonyolult, hogy már nem igazán érthető első ránézésre, akkor azt inkább szervezzük ki egy külön privát metódusba, megfelelő névvel, és a while vagy az if sokkal olvashatóbb lesz. A Java fordító pedig lesz olyan okos, és látja, hogy csak egy helyről van meghívva a kód, és majd inline befordítja a helyére.

Valóban így van? A szóbeszéd szerint a JIT majd optimalizál, de a javac nem nagyon. Nézzük meg ezt az egyszerű osztályt:

public class OptimizeThis {
	private int a(int x, int y) {
		return x + y;
	}

	public int add(int x, int y, int z) {
		return a(a(x, y), z);
	}
}

Bőven lenne mód optimalizálásra, hiszen az a() metódust akár ki is lehetne hagyni az egész mókából. Bele lehetne tenni az add() metódusba, és sokkal tömörebb és gyorsabb kódot kapnánk. Valahogy ennek megfelelően:

public class Optimized {
	public int add(int x, int y, int z) {
		return x + y + z;
	}
}

Fordítsuk le az OptimizeThis osztályt, és kérjük meg utána a javap disassemblert, hogy mutassa meg a generált kódot:

verhasp:java verhasp$ javac OptimizeThis.java
$ javap -v -p OptimizeThis.class 
Classfile /Users/verhasp/.../src/main/java/OptimizeThis.class
  Last modified 2012.07.08.; size 327 bytes
  MD5 checksum 9ba33fe0979ff0948a683fab2dc32d12
  Compiled from "OptimizeThis.java"
public class OptimizeThis
  SourceFile: "OptimizeThis.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         //  java/lang/Object."<init>":()V
   #2 = Methodref          #3.#16         //  OptimizeThis.a:(II)I
   #3 = Class              #17            //  OptimizeThis
   #4 = Class              #18            //  java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               a
  #10 = Utf8               (II)I
  #11 = Utf8               add
  #12 = Utf8               (III)I
  #13 = Utf8               SourceFile
  #14 = Utf8               OptimizeThis.java
  #15 = NameAndType        #5:#6          //  "<init>":()V
  #16 = NameAndType        #9:#10         //  a:(II)I
  #17 = Utf8               OptimizeThis
  #18 = Utf8               java/lang/Object
{
  public OptimizeThis();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return        
      LineNumberTable:
        line 1: 0

  private int a(int, int);
    flags: ACC_PRIVATE
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1       
         1: iload_2       
         2: iadd          
         3: ireturn       
      LineNumberTable:
        line 3: 0

  public int add(int, int, int);
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=4, args_size=4
         0: aload_0       
         1: aload_0       
         2: iload_1       
         3: iload_2       
         4: invokespecial #2                  // Method a:(II)I
         7: iload_3       
         8: invokespecial #2                  // Method a:(II)I
        11: ireturn       
      LineNumberTable:
        line 7: 0
}
verhasp:java verhasp$ 

Itt azt látjuk, hogy mid a két metódusunk megvan. A

  private int a(int, int);
    flags: ACC_PRIVATE
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1       
         1: iload_2       
         2: iadd          
         3: ireturn

a privát a() metódus, és

  public int add(int, int, int);
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=4, args_size=4
         0: aload_0       
         1: aload_0       
         2: iload_1       
         3: iload_2       
         4: invokespecial #2                  // Method a:(II)I
         7: iload_3       
         8: invokespecial #2                  // Method a:(II)I
        11: ireturn

a publikus add() metódus. A kód maga elég egyszerű. a() rátölti az operandus veremre az első lokális változót (iload_1) majd a második lokális változót (iload_2) és utána összeadja a kettőt (iadd) és ami így az operációs veremre kerül azzal visszatér (ireturn).

  1. a nulladik helyi változó nem statikus metódusoknál a this
  2. a metódus argumentumai is lokális változónak számítanak
  3. az első néhány lokális változó eléréséhez rövidített byte-code tartozik, mert amúgy is azokat matatja a Java program a legtöbbet
  4. mivel a példaprogramban csak int értékekkel dolgozunk, ezért nem kell bonyolítani a dolgot azzal, hogy például egy double két helyi változó slotot foglal el.

A add() metódus kódja sem sokkal bonyolultabb. Kétszer rátölti a this értékét az operációs veremre (ez kell a nem statikus a() metódus hívásához), majd az első és második lokális változót (első két metódus argumentum), és utána meghívja a 4. utasításban (61. sor) az a() metódust. Utána rárakja a harmadik lokális változót a veremre (ekkor a this és az első két változó összege van ott), és újra meghívja az a() metódust.

És akkor most nézzük meg, hogy mi generálódik az Optimized osztályból:

$ javap -v -p Optimized.class 
Classfile /Users/verhasp/.../src/main/java/Optimized.class
  Last modified 2012.07.08.; size 251 bytes
  MD5 checksum 2765acd1d55048184e9632c1a14a8e21
  Compiled from "Optimized.java"
public class Optimized
  SourceFile: "Optimized.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#12         //  java/lang/Object."<init>":()V
   #2 = Class              #13            //  Optimized
   #3 = Class              #14            //  java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               add
   #9 = Utf8               (III)I
  #10 = Utf8               SourceFile
  #11 = Utf8               Optimized.java
  #12 = NameAndType        #4:#5          //  "<init>":()V
  #13 = Utf8               Optimized
  #14 = Utf8               java/lang/Object
{
  public Optimized();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return        
      LineNumberTable:
        line 1: 0

  public int add(int, int, int);
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=4
         0: iload_1       
         1: iload_2       
         2: iadd          
         3: iload_3       
         4: iadd          
         5: ireturn       
      LineNumberTable:
        line 3: 0
}

Mennyivel egyszerűbb. És gyorsabb? A pudingpróbája, hogy megeszik. Ha nem ízlik, akkor majd megeszi a kutya. De…

Itt van még egyszer mind a két osztály kiegészítve a futtató main metódusokkal (egy-egy mindegyikben 🙂

public class OptimizeThis {
	private int a(int x, int y) {
		return x + y;
	}

	public int add(int x, int y, int z) {
		return a(a(x, y), z);
	}

	public static void main(String[] args) {
		OptimizeThis adder = new OptimizeThis();
		final int outer = 100_0000_000;
		final int loop = 100_0000_000;
		Long tStart = System.currentTimeMillis();
		for (int j = 0; j < outer; j++) {
			for (int i = 0; i < loop; i++) {
				int x = 1;
				int y = 2;
				int z = 3;
				adder.add(x, y, z);
			}
		}
		Long tEnd = System.currentTimeMillis();
		System.out.println(tEnd - tStart);
	}
}

valamint

public class Optimized {
	public int add(int x, int y, int z) {
		return x + y + z;
	}

	public static void main(String[] args) {
		Optimized adder = new Optimized();
		final int outer = 100_0000_000;
		final int loop = 100_0000_000;
		Long tStart = System.currentTimeMillis();
		for (int j = 0; j < outer; j++) {
			for (int i = 0; i < loop; i++) {
				int x = 1;
				int y = 2;
				int z = 3;
				adder.add(x, y, z);
			}
		}
		Long tEnd = System.currentTimeMillis();
		System.out.println(tEnd - tStart);
	}
}

Ezen kívül készült még egy Empty nevű osztály is, amelyikben a három szám összeadása helyett a visszatérési érték konstans nulla.

public class Empty {
	public int add(int x, int y, int z) {
		return 0;
	}

	public static void main(String[] args) {
		Empty adder = new Empty();
		final int outer = 100_0000_000;
		final int loop = 100_0000_000;
		Long tStart = System.currentTimeMillis();
		for (int j = 0; j < outer; j++) {
			for (int i = 0; i < loop; i++) {
				int x = 1;
				int y = 2;
				int z = 3;
				adder.add(x, y, z);
			}
		}
		Long tEnd = System.currentTimeMillis();
		System.out.println(tEnd - tStart);
	}
}

Van még egy futtató script is, amit a javac *.java parancs lefutása után lehet indítgatni:

#! /bin/sh
echo "Empty"
java Empty
echo "Optimized"
java Optimized
echo "OptimizeThis"
java OptimizeThis

És az eredmény:
ÁLLJ!!!! Mielőtt kinyitod, próbáld meg megbecsülni, hogy hányszor gyorsabb az optimalizált változat, mint az optimalizálatlan, és hányszor gyorsabb ennél az Empty teszt osztály. Ha megvan, akkor kinyithatod, és itt van néhány képernyőről lemásolt futtatási eredmény:

verhasp:java verhasp$ ./testrun.sh 
Empty
1970
Optimized
1987
OptimizeThis
1970
verhasp:java verhasp$ ./testrun.sh 
Empty
1986
Optimized
2026
OptimizeThis
2001
verhasp:java verhasp$ ./testrun.sh 
Empty
1917
Optimized
1892
OptimizeThis
1899
verhasp:java verhasp$ ./testrun.sh 
Empty
1908
Optimized
1903
OptimizeThis
1899
verhasp:java verhasp$ ./testrun.sh 
Empty
1898
Optimized
1891
OptimizeThis
1892
verhasp:java verhasp$ ./testrun.sh 
Empty
1896
Optimized
1896
OptimizeThis
1897
verhasp:java verhasp$ ./testrun.sh 
Empty
1897
Optimized
1903
OptimizeThis
1897
verhasp:java verhasp$ ./testrun.sh 
Empty
1908
Optimized
1892
OptimizeThis
1900
verhasp:java verhasp$ ./testrun.sh 
Empty
1899
Optimized
1905
OptimizeThis
1904
verhasp:java verhasp$ ./testrun.sh 
Empty
1891
Optimized
1896
OptimizeThis
1896
verhasp:java verhasp$ ./testrun.sh 
Empty
1895
Optimized
1891
OptimizeThis
1904
verhasp:java verhasp$ ./testrun.sh 
Empty
1898
Optimized
1889
OptimizeThis
1894
verhasp:java verhasp$ ./testrun.sh 
Empty
1917
Optimized
1894
OptimizeThis
1898
verhasp:java verhasp$ 

Konklúzió? Mielőtt az első válaszra adod le a voksod, olvasd el a lehetséges válaszokat az utolsóig!


A tesztek egy 8GB memóriával felszerelt MacBook Pro7,1 gépen futottak OS X 10.7.4, 7-es Java-val (ez utóbbit értő szem több helyen is észrevehette), de azért itt van egy ‘java -version’ kimenete:

verhasp:java verhasp$ java -version
java version "1.7.0_04"
Java(TM) SE Runtime Environment (build 1.7.0_04-b21)
Java HotSpot(TM) 64-Bit Server VM (build 23.0-b21, mixed mode)


Legközelebb nézünk majd valami érdekesebbet.