1. 程式人生 > >Java Fork Join 框架(四)效能

Java Fork Join 框架(四)效能

4效能

如今,隨著編譯器與Java虛擬機器效能的不斷提升,效能測試結果也僅僅只能適用一時。但是,本節中所提到的測試結果資料卻能揭示Fork/join框架的基本特性。

下面表格中簡單介紹了在下文將會用到的一組fork/join測試程式。這些程式是從util.concurrent包裡的示例程式碼改編而來,用來展示fork/join框架在解決不同型別的問題模型時所表現的差異,同時得到該框架在一些常見的並行測試程式上的測試結果。

程式(Program 描述(Description
Fib(菲波那契數列) 如第2節所描述的Fibonnaci程式,其中引數值為47閥值為13
Integrate(求積分)
使用遞迴高斯求積對公式**-4748的積分,i15之間的偶數。
Micro 對一種棋盤遊戲尋找最好的移動策略,每次計算出後面四次移動。
Sort(排序) 使用合併/快速排序演算法對1億數字進行排序(基於Cilk演算法)
MM(矩陣相乘) 2048*2048double型別的矩陣進行相乘
LU(矩陣分解) 4096*4096double型別的矩陣進行分解
Jacobi(雅克比迭代法) 對一個4096*4096double矩陣使用迭代方法進行矩陣鬆弛,迭代次數上限為100

下文提到的主要的測試,其測試程式都是執行在Sun Enterprise 10000伺服器上,該伺服器擁有30

CPU,作業系統為Solaris7系統,執行Solaris商業版1.2 JVM2.2.2_05釋出版本的一個早期版本)。同時,Java虛擬機器的關於執行緒對映的環境引數選擇為“bound threads(譯者注:XX:+UseBoundThreads,繫結使用者級別的執行緒到核心執行緒,只與solaris有關),而關於虛擬機器的記憶體引數設定在4.2章節討論。另外,需要注意的是下文提到的部分測試則是執行在擁有4CPUSun Enterprise450伺服器上。

為了降低定時器粒度以及Java虛擬機器啟動因素對測試結果的影響,測試程式都使用了數量巨大的輸入引數。而其它一些啟動因素我們通過在啟動定時器之前先執行初始化任務來進行遮蔽。所得到的測試結果資料,大部分都是在三次測試結果的中間值,然而一些測試資料僅僅來自一次執行結果(包括

4.2~4.4章節很多測試),因此這些測試結果會有噪音表現。

加速比(Speedups

通過使用不同數目(1~30)的工作執行緒對同一問題集進行測試,用來得到框架的擴充套件性測試結果。雖然我們無法保證Java虛擬機器是否總是能夠將每一個執行緒對映到不同的空閒CPU上,同時,我們也沒有證據來證明這點。有可能對映一個新的執行緒到CPU的延遲會隨著執行緒數目的增加而變大,也可能會隨不同的系統以及不同的測試程式而變化。但是,所得到的測試結果的確顯示出增加執行緒的數目確實能夠增加使用的CPU的數目。

加速比通常表示為“Time n/Time1.如上圖所示,其中求積分的程式表現出最好的加速比(30個執行緒的加速比為28.2),表現最差的是矩陣分解程式(30執行緒是加速比只有15.35

另一種衡量擴充套件性的依據是:任務執行率,及執行一個單獨任務(這裡的任務有可能是遞迴分解節點任務也可能是根節點任務)所開銷的平均時間。下面的資料顯示出一次性執行各個程式所得到的任務執行率資料。很明顯,單位時間內執行的任務數目應該是固定常量。然而事實上,隨著執行緒數目增加,所得到的資料會表現出輕微的降低,這也表現出其一定的擴充套件性限制。這裡需要說明的是,之所以任務執行率在各個程式上表現的巨大差異,是因其任務粒度的不同造成的。任務執行率最小的程式是Fib(菲波那契數列),其閥值設定為13,在30個執行緒的情況下總共完成了280萬個單元任務。

導致這些程式的任務完成率沒有表現為水平直線的因素有四個。其中三個對所有的併發框架來說都是普遍原因,所以,我們就從對FJTask框架(相對於Cilk等框架)所特有的因素說起,即垃圾回收。

4.2垃圾回收

總的來說,現在的垃圾回收機制的效能是能夠與fork/join框架所匹配的:fork/join程式在執行時會產生巨大數量的任務單元,然而這些任務在被執行之後又會很快轉變為記憶體垃圾。相比較於順序執行的單執行緒程式,在任何時候,其對應的fork/join程式需要最多p倍的記憶體空間(其中p為執行緒數目)。基於分代的半空間拷貝垃圾回收器(也就是本文中測試程式所使用的Java虛擬機器所應用的垃圾回收器)能夠很好的處理這種情況,因為這種垃圾回收機制在進行記憶體回收的時候僅僅拷貝非垃圾記憶體單元。這樣做,就避免了在手工併發記憶體管理上的一個複雜的問題,即跟蹤那些被一個執行緒分配卻在另一個執行緒中使用的記憶體單元。這種垃圾回收機制並不需要知道記憶體分配的源頭,因此也就無需處理這個棘手的問題。

這種垃圾回收機制優勢的一個典型體現:使用這種垃圾回收機制,四個執行緒執行的Fib程式耗時僅為5.1秒鐘,而如果在Java虛擬機器設定關閉代拷貝回收(這種情況下使用的就是標記清除垃圾回收機制了),耗時需要9.1秒鐘。

然而,只有記憶體使用率只有達到一個很高的值的情況下,垃圾回收機制才會成為影響擴充套件性的一個因素,因為這種情況下,虛擬機器必須經常停止其他執行緒來進行垃圾回收。以下的資料顯示出在三種不同的記憶體設定下(Java虛擬機器支援通過額外的引數來設定記憶體引數),加速比所表現出的差異:預設的4M半空間,64M半空間,另外根據執行緒數目按照公式(2+2pM設定半空間。使用較小的半空間,在額外執行緒導致垃圾回收率攀高的情況下,停止其他執行緒並進行垃圾回收的開銷開始影響加封。

鑑於上面的結果,我們使用64M半空間作為其他測試的執行標準。其實設定記憶體大小的一個更好的策略就是根據每次測試的實際執行緒數目來確定。(正如上面的測試資料,我們發現這種情況下,加速比會表現的更為平滑)。相對的另一方面,程式所設定的任務粒度的閥值也應該隨著執行緒數目成比例的增長。

4.3 記憶體分配和字寬

在上文提到的測試程式中,有四個程式會建立並運算元量巨大的共享陣列和矩陣:數字排序,矩陣相乘/分解以及鬆弛。其中,排序演算法應該是對資料移動操作(將記憶體資料移動到CPU快取)以及系統總記憶體頻寬,最為敏感的。為了確定這些影響因素的性質,我們將排序演算法Sort改寫為四個版本,分別對Byte位元組資料,short型資料,int型資料以及long型資料進行排序。這些程式所操作的資料都在0~255之間,以確保這些對比測試之間的平等性。理論上,操作資料的字寬越大,記憶體操作壓力也相應越大。

測試結果顯示,記憶體操作壓力的增加會導致加速比的降低,雖然我們無法提供明確的證據來證明這是引起這種表現的唯一原因。但資料的字寬的確是影響程式的效能的。比如,使用一個執行緒,排序位元組Byte資料需要耗時122.5秒,然而排序long資料則需要耗時242.5秒。

4.4 任務同步

正如3.2章節所討論的,任務竊取模型經常會在處理任務的同步上遇到問題,如果工作執行緒獲取任務的時候,但相應的佇列已經沒有任務可供獲取,這樣就會產生競爭。在FJTask框架中,這種情況有時會導致執行緒強制睡眠。

Jacobi程式中我們可以看到這類問題。Jacobi程式執行100步,每一步的操作,相應矩陣點周圍的單元都會進行重新整理。程式中有一個全域性的屏障分隔。為了明確這種同步操作的影響大小。我們在一個程式中每10步操作進行一次同步。如圖中表現出的擴充套件性的差異說明了這種併發策略的影響。也暗示著我們在這個框架後續的版本中應該增加額外的方法以供程式設計師來重寫,以調整框架在不同的場景中達到最大的效率。(注意,這種圖可能對同步因素的影響略有誇大,因為10步同步的版本很可能需要管理更多的任務區域性性)

4.5 任務區域性性

FJTask,或者說其他的fork/join框架在任務分配上都是做了優化的,儘可能多的使工作執行緒處理自己分解產生的任務。因為如果不這樣做,程式的效能會受到影響,原因有二:

從其他佇列竊取任務的開銷要比在自己佇列執行pop操作的開銷大。

在大多數程式中,任務操作操作的是一個共享的資料單元,如果只執行自己部分的任務可以獲得更好的區域性資料訪問。

如上圖所示,在大多數程式中,竊取任務的相對資料都最多維持在很低的百分比。然後其中LUMM程式隨著執行緒數目的增加,會在工作負載上產生更大的不平衡性(相對的產生了更多的任務竊取)。通過調整演算法我們可以降低這種影響以獲得更好的加速比。

4.6 與其他框架比較

與其他不同語言的框架相比較,不太可能會得到什麼明確的或者說有意義的比較結果。但是,通過這種方法,最起碼可以知道FJTask在與其他語言(這裡主要指的是CC++)所編寫的相近框架比較所表現的優勢和限制。下面這個表格展示了幾種相似框架(Cilk,Hood ,Stackthreads,以及Filaments)所測試的效能資料。涉及到的測試都是在4CPUSun Enterprise450伺服器執行4個執行緒進行的。為了避免在不同的框架或者程式上進行重新配置,所有的測試程式執行的問題集都比上面的測試稍小些。得到的資料也是取三次測試中的最優值,以確保編譯器或者說是執行時配置都提供了最好的效能。其中Fib程式沒有指定任務粒度的閥值,也就是說預設的1.(這個設定在Filaments版的Fib程式中設定為1024,這樣程式會表現的和其它版本更為一致)。

在加速比的測試中,不同框架在不同程式上所得到的測試結果非常接近,執行緒數目1~4,加速比表現在(3.0~4.0之間)。因此下圖也就只聚焦在不同框架表現的不同的絕對效能上,然而因為在多執行緒方面,所有的框架都是非常快的,大多數的差異更多的是有程式碼本身的質量,編譯器的不同,優化配置項或者設定引數造成的。實際應用中,根據實際需要選擇不同的框架以彌補不同框架之間表現的巨大差異。

FJTask在處理浮點陣列和矩陣的計算上效能表現的比較差。即使Java虛擬機器效能不斷的提升,但是相比於那些CC++語言所使用的強大的後端優化器,其競爭力還是不夠的。雖然在上面的圖表中沒有顯示,但FJTask版本的所有程式都要比那些沒有進行編譯優化的框架還是執行的快的。以及一些非正式的測試也表明,測試所得的大多數差異都是由於陣列邊界檢查,執行時義務造成的。這也是Java虛擬機器以及編譯器開發者一直以來關注並持續解決的問題。

相比較,計算敏感型程式因為編碼質量所引起的效能差異卻是很少的。