局部变量竟然比全局变量快 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 個問題:
CPU Cache 采用的是 LRU 和 Random 的清除算法,不常使用的緩存和隨機抽取一部分緩存會被刪除掉,如果正好是你用的那個全局變量呢?
CPU Cache 有緩存命中率的問題,也就是有一定的幾率會訪問不到緩存;
部分 CPU 只有兩級緩存(L1 和 L2),因此可以使用的空間是有限的。
綜上所述,我們不能把程序的執行性能完全托付給一個不那么穩定的系統硬件,所以能用局部變量堅決不要使用全局變量。
關鍵點:編寫適合你的代碼,在性能、可讀性和實用性之間,找到屬于你的平衡點!
總結
本文我們講了局部變量的和全局變量的區別,如果使用全局變量會用 getfield?關鍵字從堆中獲取變量,而局部變量則是通過出棧來獲取變量的,因為出棧操作要比堆操作快很多,因此局部變量操作也會比全局變量快很多,所以建議你使用局部變量而不是全局變量。
高手之間對決,比拼的就是細節。
最后的話原創不易,覺得有幫助,點個「在看」讓我知道,謝謝你! 往期推薦if快還是switch快?解密switch背后的秘密
本來想用“{{”秀一波,結果卻導致了內存溢出!
覺得本文有幫助,點擊“在看”鼓勵下我吧!
總結
以上是生活随笔為你收集整理的局部变量竟然比全局变量快 5 倍?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 90% 的人都会答错的面试题 == 和
- 下一篇: 实战:Redis 哨兵模式(下)