性能優化,咱們應該知道的更多一點

當咱們談到性能優化,更多的同窗可能想到的是系統層面的性能優化。好比在一個Web服務程序中,經過Redis或者其它緩存來提高網站訪問的速度等。這一方面是編譯器爲咱們作了不少優化工做,另一方面是以爲系統層面的優化效果更明顯,也更高大上。實際上,除了系統層面的性能優化外,在程序代碼層面的性能優化效果也是很是好的。 廢話很少說,咱們以事實說話。你們看一下下面兩段程序,兩段程序的做用徹底相同,就是將一個二維數組中的每個元素作加1操做。你們看一下,以爲這兩段的程序是否會有性能差別?實際測試結果是二者有近4倍的性能差別java

// 這是第一段程序
 for(i = 0; i < 1024; i ++) 
      for(j = 0; j < 1024; j ++) 
             array[j][i] ++; 
//程序的差別在這裏 ^ ^
//這是第二段程序
 for(i = 0; i < 1024; i ++) 
      for(j = 0; j < 1024; j ++) 
             array[i][j] ++; 
複製代碼

性能差別的緣由分析

你們考慮一下,爲何有如此之大的性能差別?結合代碼,咱們看到兩段代碼的差別在於對數組元素的訪問順序,前者是逐列訪問,然後者是逐行訪問。結合圖1可能會理解的更加清楚一些。而後,咱們在結合C語言中二維數據數據在內存中的排布規則(能夠在上述代碼中經過打印地址的方式驗證一下),能夠知道前者是訪問連續的地址空間,然後者訪問的是跳躍的地址空間。 數組

圖1 兩種訪問形式
以整形數組爲例,也就是說,前者訪問的地址依次爲X,X+4,X+8等等。然後者訪問的地址則依次爲X,X+4096,X+8192。後者每次跳躍4KB的地址空間。 瞭解了上述差別後,你們有沒有想到性能差別的緣由?咱們知道CPU爲了提高訪問內存的性能,在其和內存之間增長了緩存,現代CPU緩存一般爲3級緩存,分別是L一、L2和L3,其中L1和L2是CPU核獨有的,而L3是同一顆CPU的多核共享的。其基本的架構如圖2所示。
圖2 CPU緩存架構
因爲緩存
分佈式
的特色,在多個CPU之間須要保證其一致性。扯遠了,總之緩存須要切割爲比較小的粒度進行管理,這個小粒度的管理單元稱爲
緩存行
(能夠類比頁緩存中的緩存頁)。因爲緩存的容量遠遠小於內存的容量,所以緩存沒法把內存中的內容都加載其中。緩存可以其做用的最主要的緣由是利用的常規業務訪問數據的兩個特性,也就是空間局部性和時間局部性。

空間局部性:對於剛被訪問的數據,其相鄰的數據在未來被訪問的機率高。 時間局部性:對於剛被訪問的數據,其自己在未來被訪問的機率高。緩存

瞭解了上述原理,咱們就知道,對於上面程序程序代碼,因爲第二段程序依次跳躍的太遠,也就是不知足空間局部性,從而致使緩存命中失敗。也就是說第二段程序其實沒法訪問緩存中的數據,而是直接訪問的內存。而內存的訪問性能要遠遠低於緩存的訪問性能,所以就出現了文章一開始的近4倍的性能差別。性能優化

關於程序性能的其它考慮

咱們程序的很微小的改動就有可能對性能產生很是大的影響。所以,咱們在平常開發中應該到處注意代碼中是否有不恰當的代碼致使性能問題。下面咱們在列舉一個關於性能相關的程序實例,以便你們在之後的開發中參考。架構

程序結構

不合理的程序結構對性能的影響有的時候是災難性的。下面兩個函數的性能差別在字符串很長的狀況下將很是巨大。函數lower1在每次循環中都計算一下字符串的長度,而這種計算並非必要的。函數lower2則是在循環開始以前計算字符串長度,然後經過一個恆定的變量來進行條件判斷。問題的根源在於strlen函數,這個函數經過循環計算字符串的長度,若是字符串比較長,那這個函數將至關耗時。分佈式

void lower1(char *s) {
    int i;
    
    for (i = 0; i < strlen(s); i++)
        if (s[i] >='A' && s[i] <= 'Z')
            s[i] -= ('A' -'a');
}

//下面這個實現性能會更好。
void lower2(char *s) {
    int i;
    int len = strlen(s);
    
    for (i = 0; i < len; i++)
        if (s[i] >='A' && s[i] <= 'Z')
            s[i] -= ('A' -'a');
}
複製代碼

過程(函數)調用

咱們知道在過程調用的時候會存在壓棧和出棧等操做,這些操做一般都是對內存的操做,且過程比較複雜。也就是說,函數的調用過程是比較耗時的操做,儘可能減小函數調用。 值得慶幸的是現代的編譯器能夠對函數調用作不少優化工做,簡單的函數調用一般能夠被編譯器優化調。所謂優化調是隻在機器語言(彙編語言)層面已經沒有高級語言的函數調用了。 咱們經過一個具體的例子看一下,經過C語言實現一個簡單的函數調用,其中函數fun_1調用函數fun_2,而函數fun_2又調用了printf。這裏fun_2並無作什麼太多的工做,只是將兩個參數相加後傳給printf。 函數

圖3 函數調用優化
如圖所示,在gcc不作任何優化的狀況下,反彙編的代碼(圖3左下角)能夠看出,整個邏輯很是清晰,只是循序漸進的調用函數。可是,經過-O2優化後,彙編代碼變得很是簡潔了(圖3右下角), 經過fun_1的彙編代碼能夠看出它根本沒有調用fun_2,而是直接調用的printf函數。所以,在不影響其功能的狀況下,編譯器是能夠優化調函數調用的。但這不是絕對的,稍微複雜的函數調用編譯器可能就無能爲力了,而此時就可能致使性能損耗。

運算符差別

不一樣的運算的耗時差別也是很是巨大的,好比乘法的耗時是加法的兩三倍,而除法的耗時是加法的十倍以上。所以在訪問頻度比較高的邏輯中減小除法的使用將會明顯的提高。 在Java的HashMap實現中,經過位運算來計算哈希的Key,而不是經過模運算。由於模運算自己是除法運算,性能要比位運算差十倍以上。性能

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製代碼

更詳細的處理邏輯請參考JDK的源代碼,本文僅僅是拋個磚 。測試

引用與拷貝

支持類的高級語言在傳遞對象參數的時候涉及拷貝的過程,對象的拷貝也是比較消耗性能的操做。固然,高級語言經過一種成爲引用的機制實現了對象地址的傳遞,這樣就避免了拷貝的過程(這就是傳值與傳址的差別)。 在程序開發過程當中關於性能的問題還不少,本文沒法一一列舉出來。但,關鍵的問題是掌握技術的底層實現原理,任何其它高層的內容均可以經過底層原理解釋的,正所謂萬變不離其宗。優化

相關文章
相關標籤/搜索