Подтвердить что ты не робот

Правда ли, что наличие большого количества небольших методов помогает оптимизировать JIT-компилятор?

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

Я не был уверен в этом, так как кажется, что JIT-компилятор должен сам идентифицировать отдельные сегменты кода, независимо от того, являются ли они сами по себе или нет.

Может ли кто-нибудь подтвердить или опровергнуть это требование?

4b9b3361

Ответ 1

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

См. различные варианты вложения на на этой странице.


ИЗМЕНИТЬ

Выяснить немного:

  • Если метод мал, он будет вставлен в очередь, поэтому мало шансов получить штраф за разделение кода небольшими методами.
  • в некоторых случаях методы расщепления могут привести к большей инкрустации.

Пример (полный код, чтобы иметь одинаковые номера строк, если вы его попробуете)

package javaapplication27;

public class TestInline {
    private int count = 0;

    public static void main(String[] args) throws Exception {
        TestInline t = new TestInline();
        int sum = 0;
        for (int i  = 0; i < 1000000; i++) {
            sum += t.m();
        }
        System.out.println(sum);
    }

    public int m() {
        int i = count;
        if (i % 10 == 0) {
            i += 1;
        } else if (i % 10 == 1) {
            i += 2;
        } else if (i % 10 == 2) {
            i += 3;
        }
        i += count;
        i *= count;
        i++;
        return i;
    }
}

При запуске этого кода со следующими флагами JVM: -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:FreqInlineSize=50 -XX:MaxInlineSize=50 -XX:+PrintInlining (да, я использовал значения, которые подтверждают мой случай: m слишком большой, но обе рефакторированные m и m2 ниже порога - с другие значения, вы можете получить другой выход).

Вы увидите, что m() и main() скомпилированы, но m() не встает в очередь:

 56    1             javaapplication27.TestInline::m (62 bytes)
 57    1 %           javaapplication27.TestInline::main @ 12 (53 bytes)
          @ 20   javaapplication27.TestInline::m (62 bytes)   too big

Вы также можете проверить сгенерированную сборку, чтобы подтвердить, что m не встроен (я использовал эти флаги JVM: -XX:+PrintAssembly -XX:PrintAssemblyOptions=intel) - он будет выглядеть следующим образом:

0x0000000002780624: int3   ;*invokevirtual m
                           ; - javaapplication27.TestInline::[email protected] (line 10)

Если вы реорганизуете такой код (я выделил if/else в отдельном методе):

public int m() {
    int i = count;
    i = m2(i);
    i += count;
    i *= count;
    i++;
    return i;
}

public int m2(int i) {
    if (i % 10 == 0) {
        i += 1;
    } else if (i % 10 == 1) {
        i += 2;
    } else if (i % 10 == 2) {
        i += 3;
    }
    return i;
}

Вы увидите следующие действия по компиляции:

 60    1             javaapplication27.TestInline::m (30 bytes)
 60    2             javaapplication27.TestInline::m2 (40 bytes)
            @ 7   javaapplication27.TestInline::m2 (40 bytes)   inline (hot)
 63    1 %           javaapplication27.TestInline::main @ 12 (53 bytes)
            @ 20   javaapplication27.TestInline::m (30 bytes)   inline (hot)
            @ 7   javaapplication27.TestInline::m2 (40 bytes)   inline (hot)

Итак m2 вставляется в m, что вы ожидаете, поэтому мы вернемся к исходному сценарию. Но когда main скомпилируется, он фактически вписывает все это. На уровне сборки это означает, что вы больше не найдете никаких инструкций invokevirtual. Вы найдете такие строки:

 0x00000000026d0121: add    ecx,edi   ;*iinc
                                      ; - javaapplication27.TestInline::[email protected] (line 33)
                                      ; - javaapplication27.TestInline::[email protected] (line 24)
                                      ; - javaapplication27.TestInline::[email protected] (line 10)

где в основном общие инструкции "взаимозависимы".

Заключение

Я не говорю, что этот пример является репрезентативным, но он, кажется, доказывает несколько моментов:

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

И наконец: если часть вашего кода действительно важна для производительности, что важно для этих соображений, вы должны изучить выход JIT для точной настройки вашего кода и важного профиля до и после.

Ответ 2

Если вы возьмете тот же самый код и просто разбейте их на множество небольших методов, это совсем не поможет JIT.

Лучший способ сказать, что современные JVM HotSpot не наказывают вас за то, что вы пишете множество небольших методов. Они становятся агрессивными, поэтому во время выполнения вы действительно не оплачиваете стоимость вызовов функций. Это справедливо даже для invokevirtual вызовов, таких как вызов, вызывающий метод интерфейса.

Несколько лет назад я сделал сообщение в блоге, в котором описывается, как вы можете видеть, что JVM - это методы вложения. Эта техника по-прежнему применима к современным JVM. Я также счел полезным рассмотреть обсуждения, связанные с invokedynamic, где широко обсуждаются современные JVM файлы HotSpot для Java-байтового кода.

Ответ 3

Я читал многочисленные статьи, в которых говорилось, что меньшие методы (измеряемые количеством байтов, необходимых для представления метода в виде байт-кода Java), с большей вероятностью могут быть встроены JIT (компилятором с точным временем выполнения), когда он компилирует горячие методы (те, которые используются чаще всего) в машинный код. И они описывают, как метод встраивания обеспечивает лучшую производительность результирующего машинного кода. Вкратце: меньшие методы дают JIT больше возможностей с точки зрения компиляции байт-кода в машинный код, когда он идентифицирует горячий метод, и это позволяет проводить более сложные оптимизации.

Чтобы проверить эту теорию, я создал класс JMH с двумя эталонными методами, каждый из которых содержал идентичное поведение, но анализировался по-разному. Первый бенчмарк называется monolithicMethod (весь код в одном методе), а второй бенчмарк называется smallFocusedMethods и был реорганизован таким образом, чтобы каждое основное поведение было перенесено в свой собственный метод. Тест smallFocusedMethods выглядит следующим образом:

@Benchmark
public void smallFocusedMethods(TestState state) {
    int i = state.value;
    if (i < 90) {
        actionOne(i, state);
    } else {
        actionTwo(i, state);
    }
}

private void actionOne(int i, TestState state) {
    state.sb.append(Integer.toString(i)).append(
            ": has triggered the first type of action.");
    int result = i;
    for (int j = 0; j < i; ++j) {
        result += j;
    }
    state.sb.append("Calculation gives result ").append(Integer.toString(
            result));
}

private void actionTwo(int i, TestState state) {
    state.sb.append(i).append(" has triggered the second type of action.");
    int result = i;
    for (int j = 0; j < 3; ++j) {
        for (int k = 0; k < 3; ++k) {
            result *= k * j + i;
        }
    }
    state.sb.append("Calculation gives result ").append(Integer.toString(
            result));
}

и вы можете представить, как выглядит monolithicMethod (тот же код, но целиком содержащийся в одном методе). TestState просто выполняет работу по созданию нового StringBuilder (чтобы создание этого объекта не учитывалось во время тестирования) и выбора случайного числа от 0 до 100 для каждого вызова (и это было намеренно настроено так, чтобы оба тесты используют точно такую же последовательность случайных чисел, чтобы избежать риска смещения).

После выполнения теста с шестью "вилками", каждая из которых включает пять прогревов в секунду, а затем шесть итераций по пять секунд, результаты выглядят так:

Benchmark                                         Mode   Cnt        Score        Error   Units

monolithicMethod                                  thrpt   30  7609784.687 ± 118863.736   ops/s
monolithicMethod:·gc.alloc.rate                   thrpt   30     1368.296 ±     15.834  MB/sec
monolithicMethod:·gc.alloc.rate.norm              thrpt   30      270.328 ±      0.016    B/op
monolithicMethod:·gc.churn.G1_Eden_Space          thrpt   30     1357.303 ±     16.951  MB/sec
monolithicMethod:·gc.churn.G1_Eden_Space.norm     thrpt   30      268.156 ±      1.264    B/op
monolithicMethod:·gc.churn.G1_Old_Gen             thrpt   30        0.186 ±      0.001  MB/sec
monolithicMethod:·gc.churn.G1_Old_Gen.norm        thrpt   30        0.037 ±      0.001    B/op
monolithicMethod:·gc.count                        thrpt   30     2123.000               counts
monolithicMethod:·gc.time                         thrpt   30     1060.000                   ms

smallFocusedMethods                               thrpt   30  7855677.144 ±  48987.206   ops/s
smallFocusedMethods:·gc.alloc.rate                thrpt   30     1404.228 ±      8.831  MB/sec
smallFocusedMethods:·gc.alloc.rate.norm           thrpt   30      270.320 ±      0.001    B/op
smallFocusedMethods:·gc.churn.G1_Eden_Space       thrpt   30     1393.473 ±     10.493  MB/sec
smallFocusedMethods:·gc.churn.G1_Eden_Space.norm  thrpt   30      268.250 ±      1.193    B/op
smallFocusedMethods:·gc.churn.G1_Old_Gen          thrpt   30        0.186 ±      0.001  MB/sec
smallFocusedMethods:·gc.churn.G1_Old_Gen.norm     thrpt   30        0.036 ±      0.001    B/op
smallFocusedMethods:·gc.count                     thrpt   30     1986.000               counts
smallFocusedMethods:·gc.time                      thrpt   30     1011.000                   ms

Короче говоря, эти цифры показывают, что подход smallFocusedMethods выполнялся на 3,2% быстрее, а разница была статистически значимой (с достоверностью 99,9%). И обратите внимание, что использование памяти (на основе профилирования сборки мусора) существенно не отличалось. Таким образом, вы получаете более высокую производительность без увеличения накладных расходов.

Я провел множество аналогичных тестов, чтобы проверить, дают ли маленькие, сфокусированные методы лучшую пропускную способность, и обнаружил, что улучшение составляет от 3% до 7% во всех случаях, которые я пробовал. Но вполне вероятно, что фактический выигрыш сильно зависит от используемой версии JVM, распределения исполнений между вашими блоками if/else (я пошел на 90% в первый и 10% во второй, чтобы преувеличить нагрев на первое "действие", но я видел улучшения пропускной способности даже при более равном разбросе по цепочке блоков if/else) и фактическую сложность работы, выполняемой каждым из возможных действий. Поэтому не забудьте написать свои собственные конкретные тесты, если вам нужно определить, что работает для вашего конкретного приложения.

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

Ответ 4

Я действительно не понимаю, как это работает, но на основе ссылка AurA предоставлена ​​, я бы предположил, что компилятору JIT придется компилируйте меньше байт-кода, если одни и те же биты повторно используются, вместо того, чтобы скомпилировать разные байт-коды, похожие на разные методы.

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

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