1. 程式人生 > >深入理解編譯優化之迴圈展開和粗化鎖

深入理解編譯優化之迴圈展開和粗化鎖

[toc] # 簡介 之前在講JIT的時候,有提到在編譯過程中的兩種優化迴圈展開和粗化鎖,今天我們和小師妹一起從Assembly的角度來驗證一下這兩種編譯優化方法,快來看看吧。 # 迴圈展開和粗化鎖 小師妹:F師兄,上次你講到在JIT編譯的過程中會進行一些編譯上面的優化,其中就有迴圈展開和粗化鎖。我對這兩種優化方式很感興趣,能不能展開講解一下呢? 當然可以,我們先來回顧一下什麼是迴圈展開。 更多精彩內容且看: * [區塊鏈從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特幣等持續更新](http://www.flydean.com/blockchain/) * [Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新](http://www.flydean.com/learn-spring-boot/) * [Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新](http://www.flydean.com/spring5/) * [java程式設計師從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程](http://www.flydean.com/java-roadmap-2020/) 迴圈展開就是說,像下面的迴圈遍歷的例子: ~~~java for (int i = 0; i < 1000; i++) { x += 0x51; } ~~~ 因為每次迴圈都需要做跳轉操作,所以為了提升效率,上面的程式碼其實可以被優化為下面的: ~~~java for (int i = 0; i < 250; i++) { x += 0x144; //0x51 * 4 } ~~~ 注意上面我們使用的是16進位制數字,至於為什麼要使用16進位制呢?這是為了方便我們在後面的assembly程式碼中快速找到他們。 好了,我們再在 x += 0x51 的外面加一層synchronized鎖,看一下synchronized鎖會不會隨著loop unrolling展開的同時被粗化。 ~~~java for (int i = 0; i < 1000; i++) { synchronized (this) { x += 0x51; } } ~~~ 萬事具備,只欠我們的執行程式碼了,這裡我們還是使用JMH來執行。 相關程式碼如下: ~~~java @Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(value = 1, jvmArgsPrepend = { "-XX:-UseBiasedLocking", "-XX:CompileCommand=print,com.flydean.LockOptimization::test" } ) @State(Scope.Benchmark) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class LockOptimization { int x; @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE) public void test() { for (int i = 0; i < 1000; i++) { synchronized (this) { x += 0x51; } } } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(LockOptimization.class.getSimpleName()) .build(); new Runner(opt).run(); } } ~~~ 上面的程式碼中,我們取消了偏向鎖的使用:-XX:-UseBiasedLocking。為啥要取消這個選項呢?因為如果在偏向鎖的情況下,如果執行緒獲得鎖之後,在之後的執行過程中,如果沒有其他的執行緒訪問該鎖,那麼持有偏向鎖的執行緒則不需要觸發同步。 為了更好的理解synchronized的流程,這裡我們將偏向鎖禁用。 其他的都是我們之前講過的JMH的常規操作。 接下來就是見證奇蹟的時刻了。 # 分析Assembly日誌 我們執行上面的程式,將會得到一系列的輸出。因為本文並不是講解Assembly語言的,所以本文只是大概的理解一下Assembly的使用,並不會詳細的進行Assembly語言的介紹,如果有想深入瞭解Assembly的朋友,可以在文後留言。 分析Assembly的輸出結果,我們可以看到結果分為C1-compiled nmethod和C2-compiled nmethod兩部分。 先看C1-compiled nmethod: ![](https://img-blog.csdnimg.cn/20200603231112541.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 第一行是monitorenter,表示進入鎖的範圍,後面還跟著對於的程式碼行數。 最後一行是monitorexit,表示退出鎖的範圍。 中間有個add $0x51,%eax操作,對於著我們的程式碼中的add操作。 可以看到C1—compiled nmethod中是沒有進行Loop unrolling的。 我們再看看C2-compiled nmethod: ![](https://img-blog.csdnimg.cn/20200603231506361.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 和C1很類似,不同的是add的值變成了0x144,說明進行了Loop unrolling,同時對應的鎖範圍也跟著進行了擴充套件。 最後看下執行結果: ~~~java Benchmark Mode Cnt Score Error Units LockOptimization.test avgt 5 5601.819 ± 620.017 ns/op ~~~ 得分還不錯。 # 禁止Loop unrolling 接下來我們看下如果將Loop unrolling禁掉,會得到什麼樣的結果。 要禁止Loop unrolling,只需要設定-XX:LoopUnrollLimit=1即可。 我們再執行一下上面的程式: ![](https://img-blog.csdnimg.cn/20200603231931684.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 可以看到C2-compiled nmethod中的數字變成了原本的0x51,說明並沒有進行Loop unrolling。 再看看執行結果: ~~~java Benchmark Mode Cnt Score Error Units LockOptimization.test avgt 5 20846.709 ± 3292.522 ns/op ~~~ 可以看到執行時間基本是優化過後的4倍左右。說明Loop unrolling還是非常有用的。 # 總結 本文介紹了迴圈展開和粗化鎖的實際例子,希望大家能夠喜歡。 本文的例子[https://github.com/ddean2009/learn-java-base-9-to-20](https://github.com/ddean2009/learn-java-base-9-to-20) > 本文作者:flydean程式那些事 > > 本文連結:[http://www.flydean.com/jvm-jit-loop-unrolling-lock-coarsening/](http://www.flydean.com/jvm-jit-loop-unrolling-lock-coarsening/) > > 本文來源:flydean的部落格 > > 歡迎關注我的公眾號:程式那些事,更多精彩等