

局部变量竟然比全局变量快 5 倍?
source link: http://biz.51cto.com/art/202005/617305.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

哈喽,大家好,磊哥的性能优化篇又来了!
其实写这个性能优化类的文章初衷也很简单,第一:目前市面上没有太好的关于性能优化的系列文章,包括一些付费的文章;第二:我需要写一些和别人不同的知识点,比如大家都去写 SpringBoot 了,那我就不会把重点全部放在 SpringBoot 上。而性能优化方面的文章又比较少,因此这就是我写它的理由。
至于能不能用上?是不是刚需?我想每个人都有自己的答案。就像一个好的剑客,终其一生都会对宝剑痴迷,我相信读到此文的你也是一样。
回到今天的主题,这次我们来评测一下局部变量和全局变量的性能差异,首先我们先在项目中先添加 Oracle 官方提供的 JMH(Java Microbenchmark Harness,JAVA 微基准测试套件)测试框架,配置如下:
<!-- 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; } }
咦,什么鬼?这两个方法的性能不是差不多嘛!为毛,你说差 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 缓存慢很多。
局部变量为什么快?
要理解为什么局部变量会比全局变量快这个问题,我们只需要使用 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 关键字,而是使用了出栈操作来进行业务处理,而从堆中获取变量比出栈操作要慢很多,因此使用全局变量会比局部变量慢很多。关于堆、栈的内容关注公众号「Java中文社群」我在后面的 JVM 优化的章节会单独讲解。
关于缓存
有人可能会说无所谓,反正使用全局变量会使用 CPU Cache,这样性能也和局部变量差不多,那我就随便用吧,反正也差不多。
但磊哥的建议是,能用局部变量的绝不使用全局变量,因为 CPU 缓存有以下 3 个问题:
- CPU Cache 采用的是 LRU 和 Random 的清除算法,不常使用的缓存和随机抽取一部分缓存会被删除掉,如果正好是你用的那个全局变量呢?
- CPU Cache 有缓存命中率的问题,也就是有一定的几率会访问不到缓存;
- 部分 CPU 只有两级缓存(L1 和 L2),因此可以使用的空间是有限的。
综上所述,我们不能把程序的执行性能完全托付给一个不那么稳定的系统硬件,所以能用局部变量坚决不要使用全局变量。
关键点:编写适合你的代码,在性能、可读性和实用性之间,找到属于你的平衡点!
总结
本文我们讲了局部变量的和全局变量的区别,如果使用全局变量会用 getfield 关键字从堆中获取变量,而局部变量则是通过出栈来获取变量的,因为出栈操作要比堆操作快很多,因此局部变量操作也会比全局变量快很多,所以建议你使用局部变量而不是全局变量。
高手之间对决,比拼的就是细节。
作者:Java中文社群
链接:https://juejin.im/post/5ecb205451882542f010af08
来源:掘金
Recommend
-
39
-
52
我们在编程中,无时无刻地都在于方法打交道,而在方法中,我们很难不使用局部变量,比如我们有下面的这样一段很简单的代码 public void dump() { String localName; System.out.println("dump l...
-
52
局部变量 在C语言中写在{}中或者函数中或者函数的形参, 就是局部变量 Go语言中的局部变量和C语言一样 全局变量 在C语言中写在函数外面的就是全局变量 Go语言中...
-
59
byzhangxinxu from https://www.zhangxinxu.com/wordpress/?p=8417 本文可全文转载,个人网站无需授权,只要保留原作者、出处以及文中链接即可,任何网站均可摘要...
-
12
一文弄懂C语言的全局变量和局部变量,它们会冲突吗 发表于 2018-11-02 10:11...
-
11
快速了解Ajax 全局刷新和局部刷新(Ajax) 全局刷新:整个浏览器被新的数据覆盖,需要在网络中传输大量的数据。浏览器需要加载,渲染页面。相当于我们正常点击访问网站的方式。 局部刷新:在浏览器的内部,发起请求,获取数据,改变页面...
-
8
Android中全局变量与局部变量的使用总结 – Android开发中文站你的位置:Android开发中文站 > Android开发 >
-
5
Mr.Feng BlogNLP、深度学习、机器学习、Python、Go漫谈注意力机制(三):全局注意力与局部注意力Attention中的全局注意力与局部注意力。 global Attent...
-
4
局部和全局特征融合的点云显著性检测3D视觉工坊专注SLAM、点云、三维重建、结...
-
5
在被调用函数内赋值的变量是局部变量 在所有函数之外赋值的变量是局部变量 当一个函数被调用的时候,就会创建一个局部作用域,在这个函数内的所有变量都存在于该局部作用域内(global的变量除外),该函数返回时,这...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK