小師妹學JVM之:cache line對代碼性能的影響

簡介

讀萬卷書不如行萬里路,講了這麼多assembly和JVM的原理與優化,今天咱們來點不同的實戰。探索一下怎麼使用assembly來理解咱們以前不能理解的問題。java

一個奇怪的現象

小師妹:F師兄,以前你講了那麼多JVM中JIT在編譯中的性能優化,講真的,在工做中咱們真的須要知道這些東西嗎?知道這些東西對咱們的工做有什麼好處嗎?git

um...這個問題問得好,知道了JIT的編譯原理和優化方向,咱們的確能夠在寫代碼的時候稍微注意一下,寫出性能更加優秀的代碼,可是這只是微觀上了。程序員

若是將代碼上升到企業級應用,一個硬件的提高,一個緩存的加入或者一種架構的改變均可能比小小的代碼優化要有用得多。github

就像是,若是咱們的項目遇到了性能問題,咱們第一反應是去找架構上面有沒有什麼缺陷,有沒有什麼優化點,不多或者說基本上不會去深刻到代碼層面,看你的這個代碼到底有沒有可優化空間。算法

第一,只要代碼的業務邏輯不差,運行起來速度也不會太慢。spring

第二,代碼的優化帶來的收益實在過小了,而工做量又很是龐大。緩存

因此說,對於這種相似於雞肋的優化,真的有必要存在嗎?性能優化

其實這和我學習物理化學數學知識是同樣的,你學了那麼多知識,其實在平常生活中真的用不到。可是爲何要學習呢?架構

我以爲有兩個緣由,第一是讓你對這個世界有更加本質的認識,知道這個世界是怎麼運行的。第二是鍛鍊本身的思惟習慣,學會解決問題的方法。jvm

就想算法,如今寫個程序真的須要用到算法嗎?不見得,可是算法真的很重要,由於它能夠影響你的思惟習慣。

因此,瞭解JVM的原理,甚至是Assembly的使用,並非要你用他們來讓你的代碼優化的如何好,而是讓你知道,哦,原來代碼是這樣工做的。在將來的某一個,或許我就可能用到。

好了,言歸正傳。今天給小師妹介紹一個很奇怪的例子:

private static int[] array = new int[64 * 1024 * 1024];

    @Benchmark
    public void test1() {
        int length = array.length;
        for (int i = 0; i < length; i=i+1)
            array[i] ++;
    }
    @Benchmark
    public void test2() {
        int length = array.length;
        for (int i = 0; i < length; i=i+2)
            array[i] ++;
    }

小師妹,上面的例子,你以爲哪個運行的更快呢?

小師妹:固然是第二個啦,第二個每次加2,遍歷的次數更少,確定執行得更快。

好,咱們先持保留意見。

第二個例子,上面咱們是分別+1和+2,若是後面再繼續+3,+4,一直加到128,你以爲運行時間是怎麼樣的呢?

小師妹:確定是線性減小的。

好,兩個問題問完了,接下來讓咱們來揭曉答案吧。

更多精彩內容且看:

兩個問題的答案

咱們再次使用JMH來測試咱們的代碼。代碼很長,這裏就不列出來了,有興趣的朋友能夠到本文下面的代碼連接下載運行代碼。

咱們直接上運行結果:

Benchmark               Mode  Cnt   Score   Error  Units
CachelineUsage.test1    avgt    5  27.499 ± 4.538  ms/op
CachelineUsage.test2    avgt    5  31.062 ± 1.697  ms/op
CachelineUsage.test3    avgt    5  27.187 ± 1.530  ms/op
CachelineUsage.test4    avgt    5  25.719 ± 1.051  ms/op
CachelineUsage.test8    avgt    5  25.945 ± 1.053  ms/op
CachelineUsage.test16   avgt    5  28.804 ± 0.772  ms/op
CachelineUsage.test32   avgt    5  21.191 ± 6.582  ms/op
CachelineUsage.test64   avgt    5  13.554 ± 1.981  ms/op
CachelineUsage.test128  avgt    5   7.813 ± 0.302  ms/op

好吧,不夠直觀,咱們用一個圖表來表示:

從圖表能夠看出,步長在1到16之間的時候,執行速度都還相對比較平穩,在25左右,而後就隨着步長的增加而降低。

CPU cache line

那麼咱們先回答第二個問題的答案,執行時間是先平穩再降低的。

爲何會在16步長以內很平穩呢?

CPU的處理速度是有限的,爲了提高CPU的處理速度,現代CPU都有一個叫作CPU緩存的東西。

而這個CPU緩存又能夠分爲L1緩存,L2緩存甚至L3緩存。

其中L1緩存是每一個CPU核單獨享有的。在L1緩存中,又有一個叫作Cache line的東西。爲了提高處理速度,CPU每次處理都是讀取一個Cache line大小的數據。

怎麼查看這個Cache line的大小呢?

在mac上,咱們能夠執行:sysctl machdep.cpu

從圖中咱們能夠獲得,機子的CPU cache line是64byte,而cpu的一級緩存大小是256byte。

好了,如今回到爲何1-16步長執行速度差很少的問題。

咱們知道一個int佔用4bytes,那麼16個int恰好佔用64bytes。因此咱們能夠粗略的認爲,1-16步長,每次CPU取出來的數據是同樣的,都是一個cache line。因此,他們的執行速度實際上是差很少的。

inc 和 add

小師妹:F師兄,上面的解釋雖然有點完美了,可是好像還有一個漏洞。既然1-16使用的是同一個cache line,那麼他們的執行時間,應該是逐步降低纔對,爲何2比1執行時間還要長呢?

這真的是一個好問題,光看代碼和cache line好像都解釋不了,那麼咱們就從Assembly的角度再來看看。

仍是使用JMH,打開PrintAssembly選項,咱們看看輸出結果。

先看下test1方法的輸出:

再看下test2方法的輸出:

兩個有什麼區別呢?

基本上的結構都是同樣的,只不過test1使用的是inc,而test2方法使用的add。

本人對彙編語言不太熟,不過我猜二者執行時間的差別在於inc和add的差別,add可能會執行慢一點,由於它多了一個額外的參數。

總結

Assembly雖然沒太大用處,可是在解釋某些神祕現象的時候,仍是挺好用的。

本文的例子https://github.com/ddean2009/learn-java-base-9-to-20

本文做者:flydean程序那些事

本文連接:http://www.flydean.com/jvm-jit-cacheline/

本文來源:flydean的博客

歡迎關注個人公衆號:程序那些事,更多精彩等着您!

相關文章
相關標籤/搜索