天天看點

Linkedin工程師是如何優化他們的Java代碼的

原文連結:Linkedin工程師是如何優化他們的Java代碼的

最近在刷各大公司的技術部落格的時候,我在Linkedin的技術部落格上面發現了一篇很不錯博文。這篇博文介紹了Linkedin資訊流中間層Feed Mixer,它為Linkedin的Web首頁,大學首頁,公司首頁以及用戶端等多個分發管道提供支撐(如下圖所示)。

Linkedin工程師是如何優化他們的Java代碼的

在Feed Mixer裡面用到了一個叫做SPR(念“super”)的庫。博文講的就是如何優化SPR的java代碼。下面就是他們總結的優化經驗。

1. 謹慎對待Java的循環周遊

Java中的清單周遊可比它看起來要麻煩多了。就以下面兩段代碼為例:

  • A:
    private final List<Bar> _bars;
    for(Bar bar : _bars) {
        //Do important stuff
    }
               
  • B:
    privatefinalList<Bar>_bars;
    for(inti=0;i<_bars.size();i++){
    Barbar=_bars.get(i);
    //Do important stuff
    }
               

代碼A執行的時候 會為這個抽象清單建立一個疊代器,而代碼B就直接使用 

get(i)

 來擷取元素,相對于代碼A省去了疊代器的開銷。

實際上這裡還是需要一些權衡的。代碼A使用了疊代器,保證了在擷取元素的時候的時間複雜度是 O(1) (使用了 

getNext()

 和 

hasNext()

 方法),最終的時間複雜度為 O(n) 。但是對于代碼B,循環裡每次在調用 

_bars.get(i)

 的時候花費的時間複雜度為 O(n)  (假設這個list為一個 LinkedList),那麼最終代碼B整個循環的時間複雜度就是 O(n^2)  (但如果代碼B裡面的list是 

ArrayList,

 那 

get(i)

 方法的時間複雜度就是 O(1)了)。是以在決定使用哪一種周遊的方式的時候,我們需要考慮清單的底層實作,清單的平均長度以及所使用的記憶體。最後因為我們需要優化記憶體,再加上 

ArrayList

 在大多數情況下查找的時間複雜度為 O(1) ,最後決定選擇代碼B所使用的方法。

2.在初始化的時候預估集合的大小

從Java的這篇 文檔我們可以了解到: “一個HashMap 執行個體有兩個影響它性能的因素:初始大小和加載因子(load factor)。 […] 當哈希表的大小達到初始大小和加載因子的乘積的時候,哈希表會進行 rehash操作 […] 如果在一個HashMap 執行個體裡面要存儲多個映射關系時,我們需要設定足夠大的初始化大小以便更有效地存儲映射關系而不是讓哈希表自動增長讓後rehash,造成性能瓶頸。”

在Linkedin實踐的時候,常常碰到需要周遊一個 

ArrayList

 并将這些元素儲存到 

HashMap 

裡面去。将這個 

HashMap

 初始化預期的大小可以避免再次哈希所帶來的開銷。初始化大小可以設定為輸入的數組大小除以預設加載因子的結果值(這裡取0.7):

  • 優化前的代碼:
    HashMap<String,Foo> _map;
    void addObjects(List<Foo> input)
    {
      _map = new HashMap<String, Foo>(); 
      for(Foo f: input)
      {
        _map.put(f.getId(), f);
      }
    }
               
  • 優化後的代碼
    HashMap<String,Foo>_map;
    voidaddObjects(List<Foo>input)
    {
    _map=newHashMap<String,Foo>((int)Math.ceil(input.size()/0.7));
    for(Foof:input)
    {
    _map.put(f.getId(),f);
    }
    }
               

3. 延遲表達式的計算

在Java中,所有的方法參數會在方法調用之前,隻要有方法參數是一個表達式的都會先這個表達式進行計算(從左到右)。這個規則會導緻一些不必要的操作。考慮到下面一個場景:使用

ComparisonChain

比較兩個 

Foo

 對象。使用這樣的比較鍊條的一個好處就是在比較的過程中隻要一個 compareTo 方法傳回了一個非零值整個比較就結束了,避免了許多無謂的比較。例如現在這個場景中的要比較的對象最先考慮他們的score, 然後是 position, 最後就是 

_bar

 這個屬性了:

public class Foo {
private float _score;
private int _position;
private Bar _bar;
 
public int compareTo (Foo other) {
return ComparisonChain.start().
compare(_score, other.getScore()).
compare(_position, other.getPosition()).
compare(_bar.toString(), other.getBar().toString()). 
result;
}
}
           

但是上面這種實作方式總是會先生成兩個 

String

 對象來儲存 

bar.toString()

 和

other.getBar().toString()

 的值,即使這兩個字元串的比較可能不需要。避免這樣的開銷,可以為Bar 對象實作一個 comparator:

publicclassFoo{
privatefloat_score;
privateint_position;
privateBar_bar;
privatefinalBarComparatorBAR_COMPARATOR=newBarComparator();
 
publicintcompareTo(Fooother){
returnComparisonChain.start().
compare(_score,other.getScore()).
compare(_position,other.getPosition()).
compare(_bar,other.getBar(),BAR_COMPARATOR).
result();
}
privatestaticclassBarComparatorimplementsComparator<Bar>{
@Override
publicintcompare(Bara,Barb){
returna.toString().compareTo(b.toString());
}
}
}
           

4. 提前編譯正規表達式

字元串的操作在Java中算是開銷比較大的操作。還好Java提供了一些工具讓正規表達式盡可能地高效。動态的正規表達式在實踐中比較少見。在接下來要舉的例子中,每次調用

String.replaceAll()

 都包含了一個常量模式應用到輸入值中去。是以我們預先編譯這個模式可以節省CPU和記憶體的開銷。

  • 優化前:
    private String transform(String term) {
    return outputTerm = term.replaceAll(_regex, _replacement); 
    }
               
  • 優化後:
    privatefinalPattern_pattern=Pattern.compile(_regex);
    privateStringtransform(Stringterm){
    StringoutputTerm=_pattern.matcher(term).replaceAll(_replacement);
    }
               

5. 盡可能地緩存Cache it if you can

将結果儲存在緩存裡也是一個避免過多開銷的方法。但緩存隻适用于在相同資料集撒花姑娘嗎的相同資料操作(比如對一些配置的預處理或者一些字元串處理)。現在已經有多種LRU(Least Recently Used )緩存算法實作,但是Linkedin使用的是 Guava cache (具體原因見這裡) 大緻代碼如下:

privatefinalintMAX_ENTRIES=1000;
privatefinalLoadingCache<String,String>_cache;
// Initializing the cache
_cache=CacheBuilder.newBuilder().maximumSize(MAX_ENTRIES).build(newCacheLoader<String,String>(){
@Override
publicStringload(Stringkey)throwsException{
returnexpensiveOperationOn(key);
}
}
);
 
//Using the cache
Stringoutput=_cache.getUnchecked(input);
           

6. String的intern方法有用,但是也有危險

String 的 intern 特性有時候可以代替緩存來使用。

從這篇文檔,我們可以知道:

“A pool of strings, initially empty, is maintained privately by the class String. When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned”.

這個特性跟緩存很類似,但有一個限制,你不能設定最多可容納的元素數目。是以,如果這些intern的字元串沒有限制(比如字元串代表着一些唯一的id),那麼它會讓記憶體占用飛速增長。Linkedin曾經在這上面栽過跟頭——當時是對一些鍵值使用intern方法,線下模拟的時候一切正常,但一旦部署上線,系統的記憶體占用一下就升上去了(因為大量唯一的字元串被intern了)。是以最後Linkedin選擇使用 LRU 緩存,這樣可以限制最大元素數目。

最終結果

SPR的記憶體占用減少了75%,進而将feed-mixer的記憶體占用減少了 50% (如下圖所示)。這些優化減少了對象的生成,進而減少了GC得頻率,整個服務的延遲就減少了25%。

Linkedin工程師是如何優化他們的Java代碼的

注:本文翻譯自Linkedin 技術部落格。