Локальные переменные на самом деле в 5 раз быстрее, чем глобальные переменные?

Java оптимизация производительности
Локальные переменные на самом деле в 5 раз быстрее, чем глобальные переменные?

Всем привет, глава Лэй Гэ по оптимизации производительности снова здесь!

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

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

Возвращаясь к сегодняшней теме, на этот раз мы оценим разницу в производительности между локальными и глобальными переменными.Во-первых, мы сначала добавим тестовую среду JMH (Java Microbenchmark Harness, JAVA Microbenchmark Test Suite), официально предоставленную Oracle в проект.Конфигурация следующим образом:

<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
   <groupId>org.openjdk.jmh</groupId>
   <artifactId>jmh-core</artifactId>
   <version>{version}</version>
</dependency>

Затем напишите тестовый код:

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s
@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s
@Fork(1) // fork 1 个线程
@State(Scope.Thread) // 每个测试线程一个实例
public class VarOptimizeTest {

    char[] myChars = ("Oracle Cloud Infrastructure Low data networking fees and " +
            "automated migration Oracle Cloud Infrastructure platform is built for " +
            "enterprises that are looking for higher performance computing with easy " +
            "migration of their on-premises applications to the Cloud.").toCharArray();

    public static void main(String[] args) throws RunnerException {
        // 启动基准测试
        Options opt = new OptionsBuilder()
                .include(VarOptimizeTest.class.getSimpleName()) // 要导入的测试类
                .build();
        new Runner(opt).run(); // 执行测试
    }

    @Benchmark
    public int globalVarTest() {
        int count = 0;
        for (int i = 0; i < myChars.length; i++) {
            if (myChars[i] == 'c') {
                count++;
            }
        }
        return count;
    }

    @Benchmark
    public int localityVarTest() {
        char[] localityChars = myChars;
        int count = 0;
        for (int i = 0; i < localityChars.length; i++) {
            if (localityChars[i] == 'c') {
                count++;
            }
        }
        return count;
    }
}

вglobalVarTestметод использует глобальные переменныеmyCharsповторяет цикл, иlocalityVarTestметод использует локальные переменныеlocalityCharsДля прохождения цикла результаты теста JMH следующие:

img

Что за черт? Эффективность этих двух методов не одинакова! Для Мао вы говорите в 5 раз хуже?

img

CPU Cache

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

Чтобы восстановить реальную производительность (локальные переменные и глобальные переменные), нам нужно использоватьvolatileключ для ретушиmyCharsглобальная переменная, чтобы ЦП не кэшировал эту переменную,volatileПервоначальная семантика заключается в отключении кеша процессора Код, который мы изменили, выглядит следующим образом:

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s
@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s
@Fork(1) // fork 1 个线程
@State(Scope.Thread) // 每个测试线程一个实例
public class VarOptimizeTest {

    volatile char[] myChars = ("Oracle Cloud Infrastructure Low data networking fees and " +
            "automated migration Oracle Cloud Infrastructure platform is built for " +
            "enterprises that are looking for higher performance computing with easy " +
            "migration of their on-premises applications to the Cloud.").toCharArray();

    public static void main(String[] args) throws RunnerException {
        // 启动基准测试
        Options opt = new OptionsBuilder()
                .include(VarOptimizeTest.class.getSimpleName()) // 要导入的测试类
                .build();
        new Runner(opt).run(); // 执行测试
    }

    @Benchmark
    public int globalVarTest() {
        int count = 0;
        for (int i = 0; i < myChars.length; i++) {
            if (myChars[i] == 'c') {
                count++;
            }
        }
        return count;
    }

    @Benchmark
    public int localityVarTest() {
        char[] localityChars = myChars;
        int count = 0;
        for (int i = 0; i < localityChars.length; i++) {
            if (localityChars[i] == 'c') {
                count++;
            }
        }
        return count;
    }
}

Окончательный результат теста:

img

Из вышеприведенных результатов видно, чтоПроизводительность локальных переменных примерно в 5,02 раза выше, чем у глобальных переменных..

Что касается того, почему локальные переменные быстрее, чем глобальные переменные? Мы поговорим об этом позже, но сначала поговорим о кеше процессора.

В компьютерной системе,CPU Cache — это компонент, используемый для сокращения среднего времени, необходимого процессору для доступа к памяти.. Он расположен во втором нисходящем слое пирамидальной системы памяти после регистрации ЦП, как показано на следующем рисунке:

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

Кэш процессора можетКэш-память L1 (L1), кэш-память L2 (L2), а некоторые высокопроизводительные процессоры также имеют кэш-память L3 (L3)., техническая сложность и стоимость изготовления этих трех тайников относительно снижаются, поэтому их емкость также относительно увеличивается. Когда ЦП хочет прочитать часть данных, он сначала ищет в кеше L1, если он не найден, он ищет в кеше L2, и если он все еще не находит, он ищет в кеше L3 или объем памяти.

Ниже приведена сравнительная таблица времени отклика кэша и памяти на всех уровнях:

(Изображение предоставлено: cenalulu)

Как видно из приведенного выше рисункаСкорость отклика памяти намного медленнее, чем кэш процессора.

img

Почему локальные переменные быстрые?

Чтобы понять вопрос, почему локальные переменные быстрее глобальных, нам просто нужно использоватьjavacВы можете найти причину, скомпилировав их в байт-код, скомпилированный байт-код выглядит следующим образом:

javap -c VarOptimize
警告: 文件 ./VarOptimize.class 不包含类 VarOptimize
Compiled from "VarOptimize.java"
public class com.example.optimize.VarOptimize {
  char[] myChars;

  public com.example.optimize.VarOptimize();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: ldc           #7                  // String Oracle Cloud Infrastructure Low data networking fees and automated migration Oracle Cloud Infrastructure platform is built for enterprises that are looking for higher performance computing with easy migration of their on-premises applications to the Cloud.
       7: invokevirtual #9                  // Method java/lang/String.toCharArray:()[C
      10: putfield      #15                 // Field myChars:[C
      13: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #16                 // class com/example/optimize/VarOptimize
       3: dup
       4: invokespecial #21                 // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #22                 // Method globalVarTest:()V
      12: aload_1
      13: invokevirtual #25                 // Method localityVarTest:()V
      16: return

  public void globalVarTest();
    Code:
       0: iconst_0
       1: istore_1
       2: iconst_0
       3: istore_2
       4: iload_2
       5: aload_0
       6: getfield      #15                 // Field myChars:[C
       9: arraylength
      10: if_icmpge     33
      13: aload_0
      14: getfield      #15                 // Field myChars:[C
      17: iload_2
      18: caload
      19: bipush        99
      21: if_icmpne     27
      24: iinc          1, 1
      27: iinc          2, 1
      30: goto          4
      33: return

  public void localityVarTest();
    Code:
       0: aload_0
       1: getfield      #15                 // Field myChars:[C
       4: astore_1
       5: iconst_0
       6: istore_2
       7: iconst_0
       8: istore_3
       9: iload_3
      10: aload_1
      11: arraylength
      12: if_icmpge     32
      15: aload_1
      16: iload_3
      17: caload
      18: bipush        99
      20: if_icmpne     26
      23: iinc          2, 1
      26: iinc          3, 1
      29: goto          9
      32: return
}

Ключевая информацияgetfieldпо ключевому слову,getfieldСемантика здесь заключается в том, чтобы получить переменную из кучи, как видно из приведенного выше байт-кода.globalVarTestМетод передается каждый раз внутри циклаgetfieldключевое слово получает переменные из кучи, аlocalityVarTestметод не используетсяgetfieldключевое слово, но используйте операцию pop для бизнес-обработки иПолучение переменной из кучи намного медленнее, чем ее извлечение, поэтому использование глобальной переменной будет намного медленнее, чем использование локальной переменной.. Что касается содержимого кучи и стека, обратите внимание на паблик «Java Chinese Community», я объясню это отдельно в главе об оптимизации JVM позже.

О кэшировании

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

Но предложение Лея состоит в том,Никогда не используйте глобальные переменные, если вы можете использовать локальные переменные, потому что кеш процессора имеет следующие 3 проблемы:

  1. Кэш ЦП использует алгоритмы очистки LRU и Random.Кэш, который редко используется, и часть кеша, которая выбирается случайным образом, будут удалены.Что, если это будет используемая вами глобальная переменная?
  2. CPU Cache имеет проблему частоты попаданий в кэш, то есть существует определенная вероятность того, что доступ к кешу невозможен;
  3. Некоторые процессоры имеют только два уровня кэш-памяти (L1 и L2), поэтому пространство, которое можно использовать, ограничено.

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

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

Суммировать

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

Дуэль мастеров, соревнование деталей.

последние слова

Оригинальность не так проста, чувствуйте себя полезным, нажмите "отличный"Дайте мне знать, спасибо!

Подпишитесь на официальный аккаунт «Java Chinese Community» и ответьте на «Галантные товары», чтобы получить 50 оригинальных галантерейных товаров.Топ-лист.