Всем привет, глава Лэй Гэ по оптимизации производительности снова здесь!
На самом деле, первоначальный замысел написания этой статьи по оптимизации производительности также очень прост.Во-первых, в настоящее время на рынке нет хорошей серии статей по оптимизации производительности, включая несколько платных статей.Во-вторых: мне нужно написать некоторые знания, которые отличается от других.Например, все собираются писать 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 следующие:
Что за черт? Эффективность этих двух методов не одинакова! Для Мао вы говорите в 5 раз хуже?
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;
}
}
Окончательный результат теста:
Из вышеприведенных результатов видно, чтоПроизводительность локальных переменных примерно в 5,02 раза выше, чем у глобальных переменных..
Что касается того, почему локальные переменные быстрее, чем глобальные переменные? Мы поговорим об этом позже, но сначала поговорим о кеше процессора.
В компьютерной системе,CPU Cache — это компонент, используемый для сокращения среднего времени, необходимого процессору для доступа к памяти.. Он расположен во втором нисходящем слое пирамидальной системы памяти после регистрации ЦП, как показано на следующем рисунке:
Емкость кэш-памяти процессора намного меньше памяти, но скорость может быть близка к частоте процессора. Когда процессор выдает запрос на доступ к памяти, он сначала проверяет, есть ли данные запроса в кэше. Если он есть (попадание), то данные возвращаются напрямую, без обращения к памяти, если нет (сбой), то соответствующие данные в памяти должны быть сначала загружены в кэш, а затем возвращены в процессор.
Кэш процессора можетКэш-память L1 (L1), кэш-память L2 (L2), а некоторые высокопроизводительные процессоры также имеют кэш-память L3 (L3)., техническая сложность и стоимость изготовления этих трех тайников относительно снижаются, поэтому их емкость также относительно увеличивается. Когда ЦП хочет прочитать часть данных, он сначала ищет в кеше L1, если он не найден, он ищет в кеше L2, и если он все еще не находит, он ищет в кеше L3 или объем памяти.
Ниже приведена сравнительная таблица времени отклика кэша и памяти на всех уровнях:
(Изображение предоставлено: cenalulu)
Как видно из приведенного выше рисункаСкорость отклика памяти намного медленнее, чем кэш процессора.
Почему локальные переменные быстрые?
Чтобы понять вопрос, почему локальные переменные быстрее глобальных, нам просто нужно использовать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 проблемы:
- Кэш ЦП использует алгоритмы очистки LRU и Random.Кэш, который редко используется, и часть кеша, которая выбирается случайным образом, будут удалены.Что, если это будет используемая вами глобальная переменная?
- CPU Cache имеет проблему частоты попаданий в кэш, то есть существует определенная вероятность того, что доступ к кешу невозможен;
- Некоторые процессоры имеют только два уровня кэш-памяти (L1 и L2), поэтому пространство, которое можно использовать, ограничено.
В итоге,Мы не можем полностью доверять производительность выполнения программы менее стабильному системному оборудованию, поэтому мы можем использовать локальные переменные и никогда не использовать глобальные переменные..
Ключевой момент: пишите код, который вам подходит, и найдите собственный баланс между производительностью, читабельностью и удобством использования!
Суммировать
В этой статье мы говорили о разнице между локальными переменными и глобальными переменными, Если вы используете глобальные переменные, вы будете использоватьgetfield
Ключевые слова получают переменные из кучи, в то время как локальные переменные получают переменные, извлекая стек из стека. Поскольку операции извлечения намного быстрее, чем операции с кучей, операции с локальными переменными также намного быстрее, чем с глобальными переменными, поэтому рекомендуется использовать локальные переменные вместо глобальные переменные.
Дуэль мастеров, соревнование деталей.
последние слова
Оригинальность не так проста, чувствуйте себя полезным, нажмите "отличный"Дайте мне знать, спасибо!
Подпишитесь на официальный аккаунт «Java Chinese Community» и ответьте на «Галантные товары», чтобы получить 50 оригинальных галантерейных товаров.Топ-лист.