Статьи

Javac делает оптимизацию? Не похоже на это …

Мы обычно говорим, что Java-программисты должны писать код, который выглядит хорошо, и все другие проблемы решаются компилятором. Например, наличие сложного логического выражения лучше перенести в отдельный метод с хорошим именем и с одним оператором return, содержащим выражение. Оригинал, если или пока, будет намного легче понять. Компилятор Java достаточно умен, чтобы видеть, что код вызывается только из одного места и будет перемещать код внутри строки.

Это действительно так? Я слышал, что JIT-компилятор выполняет оптимизацию, а Javac-компилятор — нет. Давайте посмотрим на простой класс:

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);
    }
}

Здесь много места для оптимизации. Метод a () может быть исключен из всего забавного. Код может быть включен в метод add (), и код будет намного быстрее.
Что-то вроде этого:

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

Давайте скомпилируем класс OptimizeThis и разберем с помощью javap:

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$

Вы можете видеть, что у нас есть оба метода. Код

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 () и код

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

это открытый метод add (). Сам код прост. Метод a () загружает в стек операндов первую локальную переменную (iload_1), затем делает то же самое со второй (iload_2), а затем добавляет два (iadd). То, что осталось в стеке операндов, используется для возврата (ireturn).

  1. Локальная переменная номер ноль это в случае нестатических методов.
  2. Аргументы также рассматриваются как локальные переменные.
  3. Для первых нескольких локальных переменных существуют сокращенные байтовые коды Java, потому что сгенерированный код обращается к ним чаще всего, и это экономит пространство и скорость.
  4. Мы используем только int, и поэтому нам не нужно заботиться о более сложных проблемах, таких как двойное занятие двух слотов.

Метод add () почти такой же простой. Это загружает значение этого в стек операнда два раза. Нужно вызвать нестатический метод a () . После этого он загружает первую и вторую локальные переменные в стек (первые два аргумента метода) и в команде номер 4 (строка 61) вызывает метод a () . После этого он загружает третью локальную переменную в стек. На этот раз в стеке содержится переменная this , результат предыдущего вызова метода a () и третья локальная переменная, которая является третьим аргументом метода add () . Затем он вызывает метод a () .

Давайте посмотрим на код, сгенерированный из класса Optimized :

$ 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
}

Гораздо проще Это тоже быстрее? Доказательство пудинга в еде. Если это не вкусно, то собака съест это. Однако …

Здесь мы снова имеем два класса, расширенных некоторыми основными методами (по одному для каждого):

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);
    }
}

и

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);
    }
}

В дополнение к этому мы также создали пустой класс с именем Empty, который возвращает постоянный ноль.

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);
    }
}

Здесь у нас есть исполняющий скрипт, который можно вызвать после выполнения команды javac * .java :

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

И результат:
СТОП! Прежде чем открыть его, попробуйте оценить обоснованность между оптимизированной и неоптимизированной версией, а также насколько быстрее работает класс Empty . Если у вас есть ваша оценка, вы можете открыть результат:

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$

Заключение? Прежде чем голосовать за первый выбор, прочитайте все возможные ответы!

Нажмите здесь, чтобы посмотреть опрос.

Тесты выполнялись на 8 ГБ памяти MacBook Pro7.1 с OS X 10.7.4, 7-й Java (вы могли заметить, что это уже была Java 7). Вот вывод java-версии :

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)