喽,大家好,磊哥的性能优化篇又来了!
其实写这个性能优化类的文章初衷也很简单,第一:目前市面上没有太好的关于性能优化的系列文章,包括一些付费的文章;第二:我需要写一些和别人不同的知识点,比如大家都去写 SpringBoot 了,那我就不会把重点全部放在 SpringBoot 上。而性能优化方面的文章又比较少,因此这就是我写它的理由。
至于能不能用上?是不是刚需?我想每个人都有自己的答案。就像一个好的剑客,终其一生都会对宝剑痴迷,我相信读到此文的你也是一样。
回到今天的主题,这次我们来评测一下局部变量和全局变量的性能差异,首先我们先在项目中先添加 Oracle 官方提供的 JMH(Java Microbenchmark Harness,JAVA 微基准测试套件)测试框架,配置如下:
<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
被 CPU 缓存了,每次我们查询时不会直接从对象的实例域(对象的实际存储结构)中查询的,而是直接从 CPU 的缓存中查询的,因此才有上面的结果。
为了还原真实的性能(局部变量和全局变量),因此我们需要使用 volatile
关键来修饰 myChars
全局变量,这样 CPU 就不会缓存此变量了, volatile
原本的语义是禁用 CPU 缓存的,我们修改的代码如下:
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 缓存的事。
在计算机系统中,CPU 缓存(CPU Cache)是用于减少处理器访问内存所需平均时间的部件。在金字塔式存储体系中它位于自顶向下的第二层,仅次于 CPU 寄存器,如下图所示:
CPU 缓存的容量远小于内存,但速度却可以接近处理器的频率。当处理器发出内存访问请求时,会先查看缓存内是否有请求数据。如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器。
CPU 缓存可以分为一级缓存(L1),二级缓存(L2),部分高端 CPU 还具有三级缓存(L3),这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。当 CPU 要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。
以下是各级缓存和内存响应时间的对比图:
(图片来源:cenalulu)
从上图可以看出内存的响应速度要比 CPU 缓存慢很多。