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

这是我的第 201 期分享

作者 | 王磊

来源 | Java中文社群(ID:javacn666)

转载请联系授权(微信ID:GG_Stone)

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

其实写这个性能优化类的文章初衷也很简单,第一:目前市面上没有太好的关于性能优化的系列文章,包括一些付费的文章;第二:我需要写一些和别人不同的知识点,比如大家都去写 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(); // 执行测试}@Benchmarkpublic int globalVarTest() {int count = 0;for (int i = 0; i < myChars.length; i++) {if (myChars[i] == 'c') {count++;}}return count;}@Benchmarkpublic 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(); // 执行测试}@Benchmarkpublic int globalVarTest() {int count = 0;for (int i = 0; i < myChars.length; i++) {if (myChars[i] == 'c') {count++;}}return count;}@Benchmarkpublic 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_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: aload_05: 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:()[C10: putfield      #15                 // Field myChars:[C13: returnpublic static void main(java.lang.String[]);Code:0: new           #16                 // class com/example/optimize/VarOptimize3: dup4: invokespecial #21                 // Method "<init>":()V7: astore_18: aload_19: invokevirtual #22                 // Method globalVarTest:()V12: aload_113: invokevirtual #25                 // Method localityVarTest:()V16: returnpublic void globalVarTest();Code:0: iconst_01: istore_12: iconst_03: istore_24: iload_25: aload_06: getfield      #15                 // Field myChars:[C9: arraylength10: if_icmpge     3313: aload_014: getfield      #15                 // Field myChars:[C17: iload_218: caload19: bipush        9921: if_icmpne     2724: iinc          1, 127: iinc          2, 130: goto          433: returnpublic void localityVarTest();Code:0: aload_01: getfield      #15                 // Field myChars:[C4: astore_15: iconst_06: istore_27: iconst_08: istore_39: iload_310: aload_111: arraylength12: if_icmpge     3215: aload_116: iload_317: caload18: bipush        9920: if_icmpne     2623: iinc          2, 126: iinc          3, 129: goto          932: 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 关键字从堆中获取变量,而局部变量则是通过出栈来获取变量的,因为出栈操作要比堆操作快很多,因此局部变量操作也会比全局变量快很多,所以建议你使用局部变量而不是全局变量。

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

最后的话原创不易,觉得有帮助,点个「在看」让我知道,谢谢你!
往期推荐

if快还是switch快?解密switch背后的秘密

本来想用“{{”秀一波,结果却导致了内存溢出!

觉得本文有帮助,点击“在看”鼓励下我吧!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/546118.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

FreeMarker笔记 前言第1章 入门

简介 简介 FreeMarker是一款模板引擎&#xff1a;一种基于模板的、用来生成输出文本&#xff08;任何来自于HTML格式的文本用来自动生成源代码&#xff09;的通用工具。它是为Java程序员提供的一个开发包或者说是类库。它不是面向最终用户&#xff0c;而是为程序员提供的可以嵌…

优先级调度算法动态优先级_与优先级调度有关的问题及其解决方案

优先级调度算法动态优先级We are already familiar with what Priority Scheduling is. It is one of the most used process scheduling algorithm used in operating systems, in which every process is assigned with a priority. According to this algorithm, the proces…

hdoj 1013 Digital Roots

链接&#xff1a;zoj 1115 或 hdoj 1013 或poj 1519 虽说是水题&#xff0c;却几经波折才搞定。该题目中的数字可能非常大&#xff0c;所以不能使用整型数&#xff0c;只能采用字符变量 代码如下&#xff1a; #include <stdio.h>int digitalRoot(int n); int digitS…

厉害了,3万字的MySQL精华总结 + 面试100问!

这是我的第 202 期分享作者 | 派大新来源 | JavaKeeper&#xff08;ID&#xff1a;JavaKeeper&#xff09;分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;❝写在之前&#xff1a;不建议那种上来就是各种面试题罗列&#xff0c;然后背书式的去记忆&#x…

网页视频播放器代码大全 + 21个为您的站点和博客提供的免费视频播放器

推荐 使用 极酷 Web在线播放器。网页中嵌入视频代码综合全然版 1.avi格式 代码片断例如以下&#xff1a;  程序代码 <objectid"video"width"400"height"200"border"0"classid"clsid:CFCDAA03-8BE4-11cf-B84B-0020AFBBCCFA&q…

codejam题目_嵌套深度-Google CodeJam 2020资格回合问题解决方案

codejam题目Problem statement: 问题陈述&#xff1a; Given a string of digits S, insert a minimum number of opening and closing parentheses into it such that the resulting string is balanced and each digit d is inside exactly d pairs of matching parentheses…

hdoj 1015 Safecracker

见hdoj 1015 或 zoj 1403 或tzu 1308 我使用了枚举法&#xff0c;代码写的很罗嗦&#xff0c;可能还是深度优先搜索写起来更清晰。 /* hdoj 1015 */ #include <stdio.h> #include <string.h>#define MAX (125) #define RESLEN 5 int bubSort(int a[], int n); in…

漫话:为什么计算机起始时间是1970年1月1日?

这是我的第 203 期分享作者 | 漫画编程来源 | 漫画编程&#xff08;ID&#xff1a;mhcoding&#xff09;分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;问题复现1970-01-01对于开发者来说都是不陌生的&#xff0c;有些系统对于时间的处理如果不够好的话&…

puppeteer执行js_使用Node.js和Puppeteer与表单和网页进行交互– 1

puppeteer执行jsHi guys! Today lets look at another powerful function of the puppeteer API using Node.js. 嗨&#xff0c;大家好&#xff01; 今天&#xff0c;让我们看看使用Node.js的puppeteer API的另一个强大功能。 In the first part of this section, lets look a…

zoj 1154 Niven numbers

见zoj 1154 还是需要将输入数据当作字符串来处理&#xff0c;不能直接使用整型。 /* zoj 1154 Niven numbers */#include <stdio.h> #define MAX 100 int isNivenNum(int base, char str[]);int main(void) {int totalBlocks;int base;int first 1;char str[MAX];scanf…

面试官:不会看SQL执行计划,简历也敢写精通SQL优化?

这是我的第 204 期分享作者 | 程序员内点事来源 | 程序员内点事&#xff08;ID&#xff1a;chengxy-nds&#xff09;分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;昨天中午在食堂&#xff0c;和部门的技术大牛们坐在一桌吃饭&#xff0c;作为一个卑微技…

scrollTop的兼容性小结

2019独角兽企业重金招聘Python工程师标准>>> 在页面上加上了 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 之后&#xff0c;document.body.scrollTop的值…

stl标准模板库_C ++标准模板库(STL)中的数组及其常用功能

stl标准模板库"array" is a container in C STL, which has fixed size, which is defined in "array" header. “ array”是C STL中的一个容器&#xff0c;具有固定大小&#xff0c;在“ array”标头中定义。 Declaration: 宣言&#xff1a; array <…

zoj 1074 To the MAX

见zoj 1074 参考了别人的思路才搞定。见http://blog.csdn.net/acm_davidcn/article/details/5834454 使用了最大连续子序列和的算法&#xff0c;虽然自己也知道这个算法&#xff0c;但是却没办法做到活学活用。 /* zoj 1074 To the Max */ #include <stdio.h> #inc…

阿里巴巴为什么让初始化集合时必须指定大小?

这是我的第 205 期分享作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;哈喽&#xff0c;亲爱的小伙伴们&#xff0c;技术学磊哥&#xff0c;进步没得说&#xff01;欢迎来到…

ios页面间跳转方式总结

转自&#xff1a;http://www.cnblogs.com/anywherego/p/3542202.html 下面以OldViewController(oldC)的按钮btn点击后跳转到NewViewController(newC)为例说明: 1.Storyboard的segues方式 鼠标点击按钮btn然后按住control键拖拽到newC页面&#xff0c;在弹出的segue页面中选择跳…

__asm___错误:“”前应有'=',',',',','asm'或'_attribute_'

__asm__A very common error in C programming language, it occurs when # is not used before the include. 这是C编程语言中非常常见的错误&#xff0c;当在include之前不使用&#xff03;时&#xff0c;就会发生此错误。 As we know that #include is a preprocessor dire…

Photoshop CS3 ICO 图标保存插件

最近编程发现&#xff0c;没有啥好看的图标文件于是&#xff0c;本人使用功能强大的ps&#xff0c;制作了图标文件做后发现&#xff0c;无法保存为ico图标文件在网上搜索了半天&#xff0c;终于从茫茫网海找到ico保存插件下载存放的地方是 PS根目录 即Adobe\Adobe Photoshop CS…

zoj 1005 jugs

题目内容见zoj1005 由于A&#xff0c;B互素且A的容量小于B&#xff0c;那么可以将B装满并且倒入A中&#xff0c;如果A被装满则将A中的内容全部清空&#xff0c;一直进行下去直到某一刻B中容量恰好等于目标的容量。这种方法能得到正确的结果&#xff0c;但是通常得不到最优结果…

啪啪打脸!领导说:try-catch要放在循环体外!

这是我的第 206 期分享作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;哈喽&#xff0c;亲爱的小伙伴们&#xff0c;技术学磊哥&#xff0c;进步没得说&#xff01;欢迎来到…