tifyty

pure Java, what else ?

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.

2 responses to “A Javac optimalizál? Nem úgy tűnik…

  1. benedek1 február 18, 2013 2:23 du.

    A cikkben egy dolog van ami nem világos.
    Mit jelent a main metódussal ellátott osztályokban, a loop értékadásánál a 100_0000_000; ?
    Több helyen láttam, de magyarázatot nem találok hozzá.

Vélemény, hozzászólás?

Adatok megadása vagy bejelentkezés valamelyik ikonnal:

WordPress.com Logo

Hozzászólhat a WordPress.com felhasználói fiók használatával. Kilépés / Módosítás )

Twitter kép

Hozzászólhat a Twitter felhasználói fiók használatával. Kilépés / Módosítás )

Facebook kép

Hozzászólhat a Facebook felhasználói fiók használatával. Kilépés / Módosítás )

Google+ kép

Hozzászólhat a Google+ felhasználói fiók használatával. Kilépés / Módosítás )

Kapcsolódás: %s

%d blogger ezt kedveli: