IT资讯

局部变量竟然比全局变量快 5 倍?

作者:admin 2021-04-21 我要评论

哈喽,大家好,磊哥的性能优化篇又来了! 其实写这个性能优化类的文章初衷也很简单,第一:目前市面上没有太好的关于性能优化的系列文章,包括一些付费的文章;第...

在说正事之前,我要推荐一个福利:你还在原价购买阿里云、腾讯云、华为云服务器吗?那太亏啦!来这里,新购、升级、续费都打折,能够为您省60%的钱呢!2核4G企业级云服务器低至69元/年,点击进去看看吧>>>)

哈喽,大家好,磊哥的性能优化篇又来了!

其实写这个性能优化类的文章初衷也很简单,第一:目前市面上没有太好的关于性能优化的系列文章,包括一些付费的文章;第二:我需要写一些和别人不同的知识点,比如大家都去写 SpringBoot 了,那我就不会把重点全部放在 SpringBoot 上。而性能优化方面的文章又比较少,因此这就是我写它的理由。

至于能不能用上?是不是刚需?我想每个人都有自己的答案。就像一个好的剑客,终其一生都会对宝剑痴迷,我相信读到此文的你也是一样。

回到今天的主题,这次我们来评测一下局部变量和全局变量的性能差异,首先我们先在项目中先添加 Oracle 官方提供的 JMH(Java Microbenchmark Harness,JAVA 微基准测试套件)测试框架,配置如下:

  1. <!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core --> 
  2. <dependency> 
  3.    <groupId>org.openjdk.jmh</groupId> 
  4.    <artifactId>jmh-core</artifactId> 
  5.    <version>{version}</version> 
  6. </dependency> 

然后编写测试代码:

  1. import org.openjdk.jmh.annotations.*; 
  2. import org.openjdk.jmh.runner.Runner; 
  3. import org.openjdk.jmh.runner.RunnerException; 
  4. import org.openjdk.jmh.runner.options.Options; 
  5. import org.openjdk.jmh.runner.options.OptionsBuilder; 
  6.  
  7. import java.util.concurrent.TimeUnit; 
  8.  
  9. @BenchmarkMode(Mode.AverageTime) // 测试完成时间 
  10. @OutputTimeUnit(TimeUnit.NANOSECONDS) 
  11. @Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s 
  12. @Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s 
  13. @Fork(1) // fork 1 个线程 
  14. @State(Scope.Thread) // 每个测试线程一个实例 
  15. public class VarOptimizeTest { 
  16.  
  17.     char[] myChars = ("Oracle Cloud Infrastructure Low data networking fees and " + 
  18.             "automated migration Oracle Cloud Infrastructure platform is built for " + 
  19.             "enterprises that are looking for higher performance computing with easy " + 
  20.             "migration of their on-premises applications to the Cloud.").toCharArray(); 
  21.  
  22.     public static void main(String[] args) throws RunnerException { 
  23.         // 启动基准测试 
  24.         Options opt = new OptionsBuilder() 
  25.                 .include(VarOptimizeTest.class.getSimpleName()) // 要导入的测试类 
  26.                 .build(); 
  27.         new Runner(opt).run(); // 执行测试 
  28.     } 
  29.  
  30.     @Benchmark 
  31.     public int globalVarTest() { 
  32.         int count = 0; 
  33.         for (int i = 0; i < myChars.length; i++) { 
  34.             if (myChars[i] == 'c') { 
  35.                 count++; 
  36.             } 
  37.         } 
  38.         return count
  39.     } 
  40.  
  41.     @Benchmark 
  42.     public int localityVarTest() { 
  43.         char[] localityChars = myChars; 
  44.         int count = 0; 
  45.         for (int i = 0; i < localityChars.length; i++) { 
  46.             if (localityChars[i] == 'c') { 
  47.                 count++; 
  48.             } 
  49.         } 
  50.         return count
  51.     } 

img

咦,什么鬼?这两个方法的性能不是差不多嘛!为毛,你说差 5 倍?

CPU Cache

上面的代码之所以性能差不多其实是因为,全局变量 myChars 被 CPU 缓存了,每次我们查询时不会直接从对象的实例域(对象的实际存储结构)中查询的,而是直接从 CPU 的缓存中查询的,因此才有上面的结果。

为了还原真实的性能(局部变量和全局变量),因此我们需要使用 volatile 关键来修饰 myChars 全局变量,这样 CPU 就不会缓存此变量了, volatile 原本的语义是禁用 CPU 缓存的,我们修改的代码如下:

  1. import org.openjdk.jmh.annotations.*; 
  2. import org.openjdk.jmh.runner.Runner; 
  3. import org.openjdk.jmh.runner.RunnerException; 
  4. import org.openjdk.jmh.runner.options.Options; 
  5. import org.openjdk.jmh.runner.options.OptionsBuilder; 
  6.  
  7. import java.util.concurrent.TimeUnit; 
  8.  
  9. @BenchmarkMode(Mode.AverageTime) // 测试完成时间 
  10. @OutputTimeUnit(TimeUnit.NANOSECONDS) 
  11. @Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s 
  12. @Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s 
  13. @Fork(1) // fork 1 个线程 
  14. @State(Scope.Thread) // 每个测试线程一个实例 
  15. public class VarOptimizeTest { 
  16.  
  17.     volatile char[] myChars = ("Oracle Cloud Infrastructure Low data networking fees and " + 
  18.             "automated migration Oracle Cloud Infrastructure platform is built for " + 
  19.             "enterprises that are looking for higher performance computing with easy " + 
  20.             "migration of their on-premises applications to the Cloud.").toCharArray(); 
  21.  
  22.     public static void main(String[] args) throws RunnerException { 
  23.         // 启动基准测试 
  24.         Options opt = new OptionsBuilder() 
  25.                 .include(VarOptimizeTest.class.getSimpleName()) // 要导入的测试类 
  26.                 .build(); 
  27.         new Runner(opt).run(); // 执行测试 
  28.     } 
  29.  
  30.     @Benchmark 
  31.     public int globalVarTest() { 
  32.         int count = 0; 
  33.         for (int i = 0; i < myChars.length; i++) { 
  34.             if (myChars[i] == 'c') { 
  35.                 count++; 
  36.             } 
  37.         } 
  38.         return count
  39.     } 
  40.  
  41.     @Benchmark 
  42.     public int localityVarTest() { 
  43.         char[] localityChars = myChars; 
  44.         int count = 0; 
  45.         for (int i = 0; i < localityChars.length; i++) { 
  46.             if (localityChars[i] == 'c') { 
  47.                 count++; 
  48.             } 
  49.         } 
  50.         return count
  51.     } 

最终的测试结果是:

img

从上面的结果可以看出,局部变量的性能比全局变量的性能快了大约 5.02 倍。

至于为什么局部变量会比全局变量快?咱们稍后再说,我们先来聊聊 CPU 缓存的事。

在计算机系统中,CPU 缓存(CPU Cache)是用于减少处理器访问内存所需平均时间的部件。在金字塔式存储体系中它位于自顶向下的第二层,仅次于 CPU 寄存器,如下图所示:


CPU 缓存的容量远小于内存,但速度却可以接近处理器的频率。当处理器发出内存访问请求时,会先查看缓存内是否有请求数据。如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器。

CPU 缓存可以分为一级缓存(L1),二级缓存(L2),部分高端 CPU 还具有三级缓存(L3),这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。当 CPU 要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。

以下是各级缓存和内存响应时间的对比图:

(图片来源:cenalulu)

从上图可以看出内存的响应速度要比 CPU 缓存慢很多。

局部变量为什么快?

要理解为什么局部变量会比全局变量快这个问题,我们只需要使用 javac 把他们编译成字节码就可以找到原因了,编译的字节码如下:

  1. javap -c VarOptimize 
  2. 警告: 文件 ./VarOptimize.class 不包含类 VarOptimize 
  3. Compiled from "VarOptimize.java" 
  4. public class com.example.optimize.VarOptimize { 
  5.   char[] myChars; 
  6.  
  7.   public com.example.optimize.VarOptimize(); 
  8.     Code: 
  9.        0: aload_0 
  10.        1: invokespecial #1                  // Method java/lang/Object."<init>":()V 
  11.        4: aload_0 
  12.        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. 
  13.        7: invokevirtual #9                  // Method java/lang/String.toCharArray:()[C 
  14.       10: putfield      #15                 // Field myChars:[C 
  15.       13: return 
  16.  
  17.   public static void main(java.lang.String[]); 
  18.     Code: 
  19.        0: new           #16                 // class com/example/optimize/VarOptimize 
  20.        3: dup 
  21.        4: invokespecial #21                 // Method "<init>":()V 
  22.        7: astore_1 
  23.        8: aload_1 
  24.        9: invokevirtual #22                 // Method globalVarTest:()V 
  25.       12: aload_1 
  26.       13: invokevirtual #25                 // Method localityVarTest:()V 
  27.       16: return 
  28.  
  29.   public void globalVarTest(); 
  30.     Code: 
  31.        0: iconst_0 
  32.        1: istore_1 
  33.        2: iconst_0 
  34.        3: istore_2 
  35.        4: iload_2 
  36.        5: aload_0 
  37.        6: getfield      #15                 // Field myChars:[C 
  38.        9: arraylength 
  39.       10: if_icmpge     33 
  40.       13: aload_0 
  41.       14: getfield      #15                 // Field myChars:[C 
  42.       17: iload_2 
  43.       18: caload 
  44.       19: bipush        99 
  45.       21: if_icmpne     27 
  46.       24: iinc          1, 1 
  47.       27: iinc          2, 1 
  48.       30: goto          4 
  49.       33: return 
  50.  
  51.   public void localityVarTest(); 
  52.     Code: 
  53.        0: aload_0 
  54.        1: getfield      #15                 // Field myChars:[C 
  55.        4: astore_1 
  56.        5: iconst_0 
  57.        6: istore_2 
  58.        7: iconst_0 
  59.        8: istore_3 
  60.        9: iload_3 
  61.       10: aload_1 
  62.       11: arraylength 
  63.       12: if_icmpge     32 
  64.       15: aload_1 
  65.       16: iload_3 
  66.       17: caload 
  67.       18: bipush        99 
  68.       20: if_icmpne     26 
  69.       23: iinc          2, 1 
  70.       26: iinc          3, 1 
  71.       29: goto          9 
  72.       32: return 

其中关键的信息就在 getfield 关键字上,getfield 在此处的语义是从堆上获取变量,从上述的字节码可以看出 globalVarTest 方法在循环的内部每次都通过 getfield 关键字从堆上获取变量,而 localityVarTest 方法并没有使用 getfield 关键字,而是使用了出栈操作来进行业务处理,而从堆中获取变量比出栈操作要慢很多,因此使用全局变量会比局部变量慢很多。关于堆、栈的内容关注公众号「Java中文社群」我在后面的 JVM 优化的章节会单独讲解。

关于缓存

有人可能会说无所谓,反正使用全局变量会使用 CPU Cache,这样性能也和局部变量差不多,那我就随便用吧,反正也差不多。

但磊哥的建议是,能用局部变量的绝不使用全局变量,因为 CPU 缓存有以下 3 个问题:

  1. CPU Cache 采用的是 LRU 和 Random 的清除算法,不常使用的缓存和随机抽取一部分缓存会被删除掉,如果正好是你用的那个全局变量呢?
  2. CPU Cache 有缓存命中率的问题,也就是有一定的几率会访问不到缓存;
  3. 部分 CPU 只有两级缓存(L1 和 L2),因此可以使用的空间是有限的。

综上所述,我们不能把程序的执行性能完全托付给一个不那么稳定的系统硬件,所以能用局部变量坚决不要使用全局变量。

关键点:编写适合你的代码,在性能、可读性和实用性之间,找到属于你的平衡点!

总结

本文我们讲了局部变量的和全局变量的区别,如果使用全局变量会用 getfield 关键字从堆中获取变量,而局部变量则是通过出栈来获取变量的,因为出栈操作要比堆操作快很多,因此局部变量操作也会比全局变量快很多,所以建议你使用局部变量而不是全局变量。

高手之间对决,比拼的就是细节。

作者:Java中文社群

链接:https://juejin.im/post/5ecb205451882542f010af08

来源:掘金


本文转载自网络,原文链接:https://juejin.im/post/5ecb205451882542f010af08

版权声明:本文转载自网络,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。本站转载出于传播更多优秀技术知识之目的,如有侵权请联系QQ/微信:153890879删除

相关文章
  • 局部变量竟然比全局变量快 5 倍?

    局部变量竟然比全局变量快 5 倍?

  • 网络监控和远程办公:用户体验更为关键

    网络监控和远程办公:用户体验更为关键

  • 全系标配最新锐龙4000系列处理器 Redmi

    全系标配最新锐龙4000系列处理器 Redmi

  • 三个场景,用机器学习简化保险业务问题

    三个场景,用机器学习简化保险业务问题

腾讯云代理商
海外云服务器