Улучшает ли производительность ключевое слово final в Java?

В Java мы видим множество мест, где можно использовать ключевое слово final, но его использование является необычным.

Например:

String str = "abc";
System.out.println(str);

В приведенном выше случае str может быть final, но это обычно останавливается.

Когда метод никогда не будет переопределен, мы можем использовать ключевое слово final. Аналогично в случае класса, который не будет наследоваться.

Действительно ли использование конечного ключевого слова в любом или всех этих случаях повышает производительность? Если да, то как? Пожалуйста, объясни. Если правильное использование final действительно имеет значение для производительности, какие привычки должен разрабатывать Java-программист, чтобы наилучшим образом использовать ключевое слово?

Ответ 1

Обычно нет. Для виртуальных методов HotSpot отслеживает, действительно ли метод был переопределен, и может выполнять такие оптимизации, как inline, при условии, что метод не был переопределен - пока он не загрузит класс, который переопределяет метод, и в этот момент он может отменить (или частично отменить) эти оптимизации.

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

На мой взгляд, вы должны использовать final на основе четкого дизайна и удобочитаемости, а не по соображениям производительности. Если вы хотите изменить что-либо по соображениям производительности, вы должны выполнить соответствующие измерения, прежде чем изгибать самый чистый код вне формы - таким образом вы сможете решить, стоит ли достижение какой-либо дополнительной производительности более плохой читаемости/дизайна. (По моему опыту это почти никогда не стоит того, YMMV.)

РЕДАКТИРОВАНИЕ: Поскольку упоминались последние поля, стоит упомянуть, что они часто являются хорошей идеей в любом случае, с точки зрения четкого дизайна. Они также изменяют гарантированное поведение с точки зрения видимости поперечного потока: после завершения конструктора любые конечные поля гарантированно будут видны в других потоках немедленно. Это, пожалуй, наиболее распространенное использование final в моем опыте, хотя, как сторонник Джоша Блоха "дизайн для наследования или запрет его", я должен чаще использовать final для классов...

Ответ 2

Короткий ответ: не беспокойтесь об этом!

Длинный ответ:

Говоря о конечных локальных переменных, имейте в виду, что использование ключевого слова final поможет компилятору оптимизировать код статически, что в конечном итоге может привести к более быстрому коду. Например, окончательные строки a + b в приведенном ниже примере объединены статически (во время компиляции).

public class FinalTest {

    public static final int N_ITERATIONS = 1000000;

    public static String testFinal() {
        final String a = "a";
        final String b = "b";
        return a + b;
    }

    public static String testNonFinal() {
        String a = "a";
        String b = "b";
        return a + b;
    }

    public static void main(String[] args) {
        long tStart, tElapsed;

        tStart = System.currentTimeMillis();
        for (int i = 0; i < N_ITERATIONS; i++)
            testFinal();
        tElapsed = System.currentTimeMillis() - tStart;
        System.out.println("Method with finals took " + tElapsed + " ms");

        tStart = System.currentTimeMillis();
        for (int i = 0; i < N_ITERATIONS; i++)
            testNonFinal();
        tElapsed = System.currentTimeMillis() - tStart;
        System.out.println("Method without finals took " + tElapsed + " ms");

    }

}

Результат?

Method with finals took 5 ms
Method without finals took 273 ms

Протестировано на Java Hotspot VM 1.7.0_45-b18.

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

В отличие от времени компиляции, я не мог найти никаких доказательств того, что использование ключевого слова final оказывает какое-либо измеримое влияние на производительность.

Ответ 3

ДА, он может. Вот пример, где final может повысить производительность:

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

рассмотрим следующее:

public class ConditionalCompile {

  private final static boolean doSomething= false;

    if (doSomething) {
       // do first part. 
    }

    if (doSomething) {
     // do second part. 
    }

    if (doSomething) {     
      // do third part. 
    }

    if (doSomething) {
    // do finalization part. 
    }
}

Преобразуя атрибут doSomething в конечный атрибут, вы сказали компилятору, что всякий раз, когда он видит doSomething, он должен заменить его на false в соответствии с правилами подстановки времени компиляции. Первый проход компилятора изменяет код на что-то вроде этого:

public class ConditionalCompile {

  private final static boolean doSomething= false;

    if (false){
       // do first part. 
    }

    if (false){
     // do second part. 
    }

    if (false){
      // do third part. 
    }

    if (false){
    // do finalization part. 

    }
}

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

public class ConditionalCompile {


  private final static boolean doSomething= false;

  public static void someMethodBetter( ) {

    // do first part. 

    // do second part. 

    // do third part. 

    // do finalization part. 

  }
}

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

Ответ 5

Вы действительно спрашиваете о двух (по крайней мере) разных случаях:

  • final для локальных переменных
  • final для методов/классов

Джон Скит уже ответил 2). Примерно 1):

Я не думаю, что это имеет значение; для локальных переменных компилятор может определить, является ли эта переменная окончательной или нет (просто проверяя, назначена ли она более одного раза). Поэтому, если компилятор хотел оптимизировать переменные, которые назначаются только один раз, он может сделать это независимо от того, действительно ли объявлена ​​переменная final или нет.

final может иметь значение для полей protected/public class; компилятору очень сложно выяснить, установлено ли поле более одного раза, поскольку это может произойти из другого класса (который, возможно, даже не был загружен). Но даже тогда JVM может использовать технику, которую описывает Jon (оптимизируйте оптимистично, верните, если класс загружен, который меняет поле).

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

Edit:

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

Ответ 6

Примечание. Не эксперт по Java

Если я правильно помню свою java, было бы очень мало способов повысить производительность, используя ключевое слово final. Я всегда знал, что он существует для "хорошего кода" - дизайна и удобочитаемости.

Ответ 7

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

Ответ 8

Собственно, при тестировании некоторого кода, связанного с OpenGL, я обнаружил, что использование последнего модификатора в закрытом поле может ухудшить. Вот начало тестирования класса I:

public class ShaderInput {

    private /* final */ float[] input;
    private /* final */ int[] strides;


    public ShaderInput()
    {
        this.input = new float[10];
        this.strides = new int[] { 0, 4, 8 };
    }


    public ShaderInput x(int stride, float val)
    {
        input[strides[stride] + 0] = val;
        return this;
    }

    // more stuff ...

И это метод, который я использовал для проверки производительности различных альтернатив, среди которых класс ShaderInput:

public static void test4()
{
    int arraySize = 10;
    float[] fb = new float[arraySize];
    for (int i = 0; i < arraySize; i++) {
        fb[i] = random.nextFloat();
    }
    int times = 1000000000;
    for (int i = 0; i < 10; ++i) {
        floatVectorTest(times, fb);
        arrayCopyTest(times, fb);
        shaderInputTest(times, fb);
        directFloatArrayTest(times, fb);
        System.out.println();
        System.gc();
    }
}

После третьей итерации, когда VM разогрелась, я последовательно получил эти цифры без заключительного ключевого слова:

Simple array copy took   : 02.64
System.arrayCopy took    : 03.20
ShaderInput took         : 00.77
Unsafe float array took  : 05.47

С ключевое слово final:

Simple array copy took   : 02.66
System.arrayCopy took    : 03.20
ShaderInput took         : 02.59
Unsafe float array took  : 06.24

Обратите внимание на цифры для теста ShaderInput.

Неважно, сделало ли я поля общедоступными или частными.

Кстати, есть еще несколько непонятных вещей. Класс ShaderInput превосходит все остальные варианты, даже с ключевым словом final. Это замечательный b/c, в основном это класс, который переносит массив с плавающей точкой, в то время как другие тесты напрямую управляют массивом. Придется это понять. Может иметь какое-то отношение к свободному интерфейсу ShaderInput.

Также System.arrayCopy на самом деле, по-видимому, несколько медленнее для небольших массивов, чем просто копирование элементов из одного массива в другой в цикле for. И использование sun.misc.Unsafe(а также прямой java.nio.FloatBuffer, не показано здесь) выполняется с ужасом.

Ответ 9

Определенно да, если переменная преобразуется как константа,

поскольку мы знаем, что java-компилятор преобразует такие конечные переменные, как константа, которая позволяет

поскольку концепция постоянного java-компилятора напрямую заменяет значение ссылкой на время компиляции

в java-переменных идет как постоянный, если это строковый или примитивный тип, который задан без какого-либо процесса выполнения

в противном случае это просто конечная (non changeble) переменная,

& Постоянное использование всегда быстрее, чем ссылка.

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

Ответ 10

Я поражен тем, что на самом деле никто не опубликовал какой-то настоящий код, который декомпилируется, чтобы доказать, что есть хотя бы незначительная разница.

Для справки это было протестировано против javac версии 8, 9 и 10.

Предположим, что этот метод:

public static int test() {
    /* final */ Object left = new Object();
    Object right = new Object();

    return left.hashCode() + right.hashCode();
}

Компилируя этот код как есть, выдает тот же самый байтовый код, что и при final (final Object left = new Object();).

Но этот:

public static int test() {
    /* final */ int left = 11;
    int right = 12;
    return left + right;
}

Выдает:

   0: bipush        11
   2: istore_0
   3: bipush        12
   5: istore_1
   6: iload_0
   7: iload_1
   8: iadd
   9: ireturn

Выход из final, который будет присутствовать, вызывает:

   0: bipush        12
   2: istore_1
   3: bipush        11
   5: iload_1
   6: iadd
   7: ireturn

Код в значительной степени не требует пояснений, если существует постоянная времени компиляции, он будет загружаться непосредственно в стек операнда (он не будет храниться в массиве локальных переменных, как в предыдущем примере, через bipush 12; istore_0; iload_0). - какой смысл имеет смысл, поскольку никто не может его изменить.

С другой стороны, почему во втором случае компилятор не создает istore_0 ... iload_0 вне меня, он не похож на то, что слот 0 используется каким-либо образом (он может сжимать массив переменных таким образом, но может быть Im пропуская некоторые внутренние детали, не могу точно сказать)

Я был удивлен, увидев такую ​​оптимизацию, учитывая, как мало javac. Что же мы должны всегда использовать final? Я даже не собираюсь писать тест JMH (который я хотел изначально), я уверен, что diff находится в порядке ns (если возможно вообще быть захвачен). Единственное место, где это может быть проблемой, - это когда метод не может быть встроен из-за его размера (и объявление final уменьшит этот размер на несколько байтов).

Есть еще два final, которые необходимо устранить. Во-первых, когда метод final (с точки зрения JIT), такой метод является мономорфным - и это самые любимые с помощью JVM.

Затем существуют переменные экземпляра final (которые должны быть установлены в каждом конструкторе); они важны, так как они гарантируют корректно опубликованную ссылку как это здесь немного коснуться), а также точно указано JLS.

Ответ 11

final ключевое слово может использоваться пятью способами в Java.

  • Класс является окончательным
  • Контрольная переменная окончательна
  • Локальная переменная окончательна
  • Метод окончательный

Класс является окончательным: класс является окончательным, мы не можем продлить или наследование означает, что наследование невозможно.

Аналогично. Объект является окончательным: некоторое время мы не изменяем внутреннее состояние объекта, поэтому в таком случае мы можем указать, что объект является конечным объектом. Объект final означает, что переменная не является окончательной.

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