Будет ли Java встроенный метод во время оптимизации?

Интересно, достаточно ли JVM/javac, чтобы превратить

// This line...
string a = foo();

string foo()
{
  return bar();
}

string bar()
{
  return some-complicated-string computation;
}

в

string a = bar();

Или разделите ненужный вызов на foo() в случае релиза (потому что недостижимый код):

string a = foo(bar());

// bar is the same
...

string foo(string b)
{
  if (debug) do-something-with(b);
}

Я чувствую, что да для первого примера и "не так уверен" для второго, но может ли кто-нибудь дать мне несколько указателей/ссылок, чтобы подтвердить это?

Ответ 1

javac будет представлен байт-код, который является точным представлением исходной Java-программы, которая генерирует байт-код (за исключением некоторых ситуаций, когда он может оптимизировать: постоянное сгибание и удаление мертвого кода). Однако оптимизация может выполняться JVM, когда он использует компилятор JIT.

Для первого сценария похоже, что JVM поддерживает inlining (см. ниже в разделе Методы и см. здесь для примера inline для JVM).

Я не мог найти никаких примеров применения метода, выполняемого самим javac. Я попробовал скомпилировать несколько примеров программ (похожих на тот, который вы описали в своем вопросе), и ни один из них, казалось, напрямую не ввел метод, даже когда он был final. Казалось бы, такие оптимизаторы выполняются JVM JIT-компилятором, а не javac. "Компилятор", упомянутый в разделе Методы здесь, кажется, является компилятором JSM JIT для HotSpot, а не javac.

Из того, что я вижу, javac поддерживает устранение мертвого кода (см. пример для второго случая) и постоянную фальцовку. При постоянном складывании компилятор будет предварительно вычислять константные выражения и использовать вычисляемое значение вместо выполнения вычисления во время выполнения. Например:

public class ConstantFolding {

   private static final int a = 100;
   private static final int b = 200;

   public final void baz() {
      int c = a + b;
   }
}

компилируется следующий байт-код:

Compiled from "ConstantFolding.java"
public class ConstantFolding extends java.lang.Object{
private static final int a;

private static final int b;

public ConstantFolding();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public final void baz();
  Code:
   0:   sipush  300
   3:   istore_1
   4:   return

}

Обратите внимание, что байт-код имеет sipush 300 вместо aload getfield и iadd. 300 - вычисленное значение. Это также относится к переменным private final. Если a и b не были статичными, итоговый байт-код будет выглядеть следующим образом:

Compiled from "ConstantFolding.java"
public class ConstantFolding extends java.lang.Object{
private final int a;

private final int b;

public ConstantFolding();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   bipush  100
   7:   putfield    #2; //Field a:I
   10:  aload_0
   11:  sipush  200
   14:  putfield    #3; //Field b:I
   17:  return

public final void baz();
  Code:
   0:   sipush  300
   3:   istore_1
   4:   return

}

Здесь также используется sipush 300.

Во втором случае (удаление мертвого кода) я использовал следующую тестовую программу:

public class InlineTest {

   private static final boolean debug = false;

   private void baz() {
      if(debug) {
         String a = foo();
      }
   }

   private String foo() {
      return bar();
   }

   private String bar() {
      return "abc";
   }
}

который дает следующий байт-код:

Compiled from "InlineTest.java"
public class InlineTest extends java.lang.Object{
private static final boolean debug;

public InlineTest();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

private void baz();
  Code:
   0:   return

private java.lang.String foo();
  Code:
   0:   aload_0
   1:   invokespecial   #2; //Method bar:()Ljava/lang/String;
   4:   areturn

private java.lang.String bar();
  Code:
   0:   ldc #3; //String abc
   2:   areturn

}

Как вы можете видеть, foo вообще не вызывается в baz, потому что код внутри блока if эффективно "мертв".

Sun (теперь Oracle) HotSpot JVM сочетает в себе интерпретацию байт-кода, а также компиляцию JIT. Когда байт-код представлен JVM, код изначально интерпретируется, но JVM будет контролировать байт-код и выделять часто используемые части. Он скрывает эти части в собственный код, чтобы они работали быстрее. Для части байт-кода, которые не используются так часто, эта компиляция не выполняется. Это так же хорошо, потому что компиляция имеет некоторые накладные расходы. Так что это действительно вопрос компромисса. Если вы решите скомпилировать весь байт-код в nativecode, тогда код может иметь очень долгую задержку запуска.

В дополнение к мониторингу байт-кода, JVM также может выполнять статический анализ байт-кода, поскольку он интерпретирует и загружает его для дальнейшей оптимизации.

Если вы хотите знать конкретные виды оптимизаций, которые выполняет JVM, эта страница в Oracle очень полезна. В нем описываются методы производительности, используемые в JVM HotSpot.

Ответ 2

в том же файле класса javac сможет встраивать static и final (другие файлы классов могут изменять встроенную функцию)

однако JIT сможет оптимизировать гораздо больше (включая вложение лишних удалений границ и нулевых проверок и т.д.), поскольку он знает больше о коде

Ответ 3

"Высокооптимизирующий" JIT-компилятор включит оба случая (и, @Mysticial, он может даже встраивать некоторые полиморфные случаи, используя различные формы обмана).

Вы можете увеличить шансы вложения, сделав методы окончательными и несколько других трюков.

javac выполняет некоторую примитивную вставку, в основном из окончательных/частных методов, в первую очередь предназначенную для помощи в некоторых парадигмах условной компиляции.

Ответ 4

JVM, скорее всего, будет встроен. В целом лучше всего оптимизировать для чтения человеком. Пусть JVM выполняет оптимизацию времени выполнения.

Эксперт JVM Брайан Гетц говорит final не влияет на встроенные методы.

Ответ 5

Если вы выбросили исключение в bar() и распечатаете stacktrace, вы увидите весь путь вызовов... Я думаю, что java уважает их всех.

Второй случай тот же, debug - это просто переменная вашей системы, а не определение как в С++, поэтому ее необходимо предварительно оценить.

Ответ 6

Возможно, я ошибаюсь, но мое чувство "нет во всех случаях". Поскольку ваш string bar() может быть переопределен перегрузкой другими классами в одном пакете. final - хорошие кандидаты, но это зависит от JIT.

Еще одно интересное замечание: здесь.