做者簡介java
Dong Lea任職於紐約州立大學奧斯威戈分校(State University of New York at Oswego),他發佈了第一個普遍使用的java collections框架實現,他實現了java.concurrent.*(JDK5開始至今)。ios
論文譯文開始:程序員
論文摘要算法
本論文介紹一種支持並行編程方式的Java框架,主要包括設計、實現和性能分析三個部分。基於它,一個任務被(遞歸的)劃分爲並行執行的子任務,父任務等待子任務的執行完成,並組裝最後結果。整體設計是Cilk語言採用的work-stealing框架的一個變體,主要技術性能關注點包括:構建和管理任務隊列、工做線程。測試結果顯示,對於大部分問題它有很大的並行速度提高,測試也預示了可能的性能改進。編程
一、介紹設計模式
Fork/Join並行是得到良好的並行性能的最簡單高效的設計技術。他是分治(Divide-and-Conquer)算法的的並行實現,它的典型應用形式:數組
Result solve(Problem problem) {
if (problem is small)
directly solve problem
else {
split problem into independent parts
fork new subtasks to solve each part
join all subtasks
compose result from subresults
}
}
fork操做啓動一個新的並行fork/join子任務,join操做則致使當前線程等待直到子任務執行完成。fork/join算法,就像其它分治算法同樣,幾乎常常是不斷的遞歸,重複劃分子任務——直到它們的規模小到可以使用簡單、簡短的串行方法完成。一些相關的編程技術和實例在《Java併發編程——設計原則與模式》第二版(Concurrent Programming in Java, second edition ) [7] 4.4章節中已經討論過。本篇論文將討論FJTask的設計(第2節)、實現(第3節)以及性能(第4節),它是一個支持並行編程的Java框架。FJTask 做爲util.concurrent軟件包的一部分,目前能夠在http://gee.cs.oswego.edu.獲取到。緩存
二、設計性能優化
Fork/Join 程序能夠在任何支持並行建立、執行子任務的框架上運行——只要它有一種等待子任務完成的同步機制。然而,java.lang.Thread類(固然也包括POSIX線程——它們經常是Java線程的基礎)由於如下緣由,它僅僅是支持fork/join編程的次優選擇:數據結構
(1)fork/join任務只有簡單和常規的同步和管理需求。fork/join任務處理的計算圖顯示,相對於普通線程它允許更加高效的調度策略。舉個例子,fork/join任務除了在等待子任務返回結果時,其它狀況下不須要同步。所以,普通線程所須要的狀態記錄和開銷在這裏是一種浪費。
(2)對於一個合理的基本任務粒度,構造和管理線程的時間將超越任務的計算時間自己。儘管粒度能夠也應該隨着特定平臺調整,但極端的粗粒度加劇了線程的開銷,從而限制了得到高並行性的可能。
一句話,標準線程框架對於大部分fork/join程序來講是一種資源浪費。因爲線程是許多其它併發和並行編程的基礎,爲了可以支持這種(fork/join)風格的程序,移除現有線程的「冗餘」或者調整它的調度策略都是不合理的。儘管如此——問題的解決方案由來已久:第一個系統的解決方法來自於Cilk語言。Cilk和其它的輕量級框架在操做系統的基本線程之上,構建了一個支持fork/join程序的支撐層。提供一個支持Java的輕量級運行框架的意義在於:讓fork/join程序具備更好的移植性,從而容許它在支持JVM的普遍平臺上運行。
圖一 FJTask的基本處理流程
FJTask框架是基於Cilk設計方案的一個變體,其它的變體出如今Hood, Filaments, stackthreads以及其它相關的基於輕量級的任務中。全部的這些框架將任務映射到線程的機制相似於操做系統將任務分配處處理器上的方式——同時,它們也兼顧了處理映射時fork/join程序的簡潔性、常規性、侷限性,它們都能容納(延伸)不一樣風格的並行編程。fork/join設計的優化包括:
(1)建立了一個worker線程的線程池。每一個工做線程(「重」線程,Java中是Thread的子類FJTaskRunner)處理隊列中的任務,一般工做線程數和CPU核心同樣多。在本地化的框架中(如Cilk),它們被映射爲內核線程或者輕量級線程,依次提交給CPU。在Java中,JVM和操做系統相互協做將程序提交到CPU。由於這些任務是計算密集型的,任何合理的映射策略會將這些線程映射到不一樣CPU核心。
(2)全部的fork/join任務都是輕量級可執行類,它們不是Threads實例。在Java中,獨立可執行類必須實現Runnable接口、重寫run方法。FJTask框架中,全部這些任務都是FJTask的子類(不是Thread),它們都實現了Runnable。(兩種狀況:一個類能夠選擇實現Runnable而且將實例運行於任務或者線程,由於任務運行於FJTask方法的嚴格限制中,所以擴展FJTask子類更方便直接調用)
(3)一個特殊用途的隊列和調度機制用來管理任務,而且經過Worker線程來執行它們(具體見章節2.1)。這些機制被任務類的少許方法觸發:fork, join, isDone(是否完成狀態指示器),以及一些便捷的方法,好比coInvoke建立並join兩個或多個任務。
(4)一個簡單的控制和管理基礎設施(FJTaskRunnerGroup)設置worker線程池,而且初始化執行給定的fork/join任務,當它被一個常規線程調用時(好比某個Java程序的main方法)
做爲讓程序員瞭解這個框架的標準示例,這裏有一個計算斐波拉契函數的類(注意:在Java 8 中,對應的是ForkJoinTask類):
package java.util;
class Fib extends FJTask {
static final int threshold = 13;
volatile int number; // arg/result
Fib(int n) {
number = n;
}
int getAnswer() {
if (!isDone())
throw new IllegalStateException();
return number;
}
public void run() {
int n = number;
if (n <= threshold) //
number = seqFib(n);
else {
Fib f1 = new Fib(n −
Fib f2 = new Fib(n −
coInvoke(f1, f2);
number = f1.number +
}
}
public static void main(String[] args) {
try {
int groupSize = 2; // for example
FJTaskRunnerGroup group = new FJTaskRunnerGroup(groupSize);
Fib f = new Fib(35); // for example
group.invoke(f);
int result = f.getAnswer();
System.out.println("Answer: " + result);
} catch (InterruptedException ex) {
}
}
int seqFib(int n) {
if (n <= 1) return n;
else return seqFib(n−1) + seqFib(n−2);
}
}
這個版本比基於java.lang.Thread(每一個子任務新建一個線程)的快30倍(第四節中的平臺)。達到這樣性能的同時,它還保持了Java多線程程序內在的可移植性。程序員一般只對兩個可調的參數感興趣:
(1)構造的工做線程數目——一般應當與對應平臺CPU的核心數一致(爲了給相關的工做保留CPU,能夠少些;爲了填補非計算型任務的CPU閒置,能夠多些)。
(2)粒度參數——表示某一時刻,產生任務的開銷與可能的並行計算效益的比例。這個參數經常與算法相關——而不是平臺。一般在單CPU上設定一個閾值可以產生好的結果,移植到多CPU平臺也能很好表示。做爲一個附帶的好處,這個機制與JVM的動態編譯機制(相較於複雜方法,它更能優化短小的方法)很好的協調。以上特性以及數據的局部性特色,使得fork/join算法即使在單處理器上也能得到超越其它算法的性能。
2.1 工做竊取
fork/join框架的核心來自於它的輕量級調度機制。FJTask借鑑在Cilk中採用的工做竊取調度策略:
(1)每一個worker線程利用它本身的調度隊列維護可執行任務。
(2)隊列是雙端的,支持LIFO(last-in-first-out)的push和pop操做,通知也支持FIFO(first-in-first-out)的take操做。
(3)一個任務fork的子任務,只會push到它所在線程的隊列。
(4)工做線程使用LIFO經過pop處理它本身隊列中的線程。
(5)當線程本身本地隊列中沒有待處理任務時,它嘗試去隨機讀取(竊取)一個worker線程的工做隊列任務(使用FIFO)。
(6)當線程進入join操做,它開始處理其它線程的任務(本身的已經處理完了),直到目標任務完成(經過isDone方法)。所以,全部任務都無阻塞的完成了。
(7)當一個工做線程沒有任務了,而且嘗試從其它線程處竊取也失敗了,它讓出資源(經過使用yields, sleeps或者其它優先級調整)而且隨後會再次激活,直到全部工做線程都空閒了——此時,它們都阻塞在等待另外一個頂層線程的調用。
如同在第5節中詳細討論的,使用LIFO策略處理線程本身的任務,竊取其它工做線程任務時卻使用FIFO策略——這是普遍使用的一類遞歸fork/join設計的優化機制。不太正式的總結下,這種模式有如下兩個優勢:它經過竊取工做線程隊列反方向的任務減小了競爭。同時,它利用了遞歸的分治算法越早的產生大任務這一特色。所以,老的竊取線程會拿到更大的任務,這導致問題進一步的遞歸分解(竊取線程的子任務將進入它本身的本地隊列)。這些規則的結果,擁有相對細粒度的基本任務,比那些僅僅使用粗粒度劃分或沒有使用遞歸分解的任務運行更快。雖然,對大部分fork/join框架來講,不多一部分任務被竊取,建立許多細粒度任務意味着當worker線程準備好事就能運行它們。
3. 實現
這個框架使用800行純Java代碼實現,主要在類FJTaskRunner中(它是java.lang.Thread的子類)。FJTasks主要持有一個布爾型的完成狀態變量,它將全部其它操做交給當前的工做線程。FJTaskRunnerGroup類用來產生工做線程,維持一些共享狀態(全部線程的身份,須要進行竊取操做等),幫助協調啓動和關閉狀態。
更詳細的實現文檔在util.concurrent包中,本節主要討論實現這個框架面臨的兩種問題:支持高效的雙端隊列操做(push, pop, take),管理竊取協議(接下來哪一個線程將得到一個新任務)。
3.1 雙端隊列
爲保持高效和可伸縮的執行,任務管理必須越快越好。建立,push和隨後的pop(或者是頻率較低的take)任務相似於串行程序中的過程調用開銷。小開銷允許程序員保持線程的細粒度,從而得到更好的並行度。任務分配自己是JVM的職責,Java垃圾收集機制讓咱們不須要建立特殊用途的內存分配器來維持任務執行。這大大減小了實現FJTasks的代碼量和複雜度——相比與其它語言實現的相似框架。
雙端隊列的結構利用了一種通用的模式,即每一個隊列都使用了一個大小可調數組,以及兩個索引:top索引的職責如同一個基於數組的棧指針,它隨着push和top操做而改變;base索引只能經過take操做修改。因爲FJTaskRunner的操做都與雙端隊列的具體細節(好比,fork簡單的調用push)緊密相關,這個數據結構直接嵌入到FJTaskRunner而不是單獨定義一個組件。
由於雙端隊列的元素實際被多個線程訪問,有時沒有充分同步,而且單獨的數組元素不能被聲明爲volatile,每一個數組元素其實是保持volatile引用的小轉發對象的固定索引。這個決定一開始是爲了與Java內存規則一致,但它引入的間接性在測試平臺上偏偏提升了程序性能,緣由多是它減小了訪問緩存相鄰元素的競爭,間接性會使得元素在內存中更加分散。
實現雙端隊列的主要挑戰來自於同步和減小競爭。即便時在同步設施優化的JVM中[2],每個push和pop操做都須要獲取鎖成爲一個性能瓶頸。然而,Cilk[5]中的改變策略基於如下觀察提供了一種解決方案:
(1)push和pop操做僅僅被隊列擁有者線程調用。
(2)訪問take操做能夠簡單的每次侷限於一個竊取線程(經過設置take進入鎖——它也是必要時禁止take操做的鎖)。由此,衝突控制轉變爲兩部同步問題。
(3)pop和take操做只有在雙端隊列將要變爲空時衝突,其它狀況,它們被確保在不一樣的數組元素上操做。
將top和base索引定義位volatile確保pop和take操做在雙端隊列中大於一個元素時,不須要鎖定。這是經過一種Dekker風格的算法,push操做會先遞減top:
if(--top) >= base) ...
take操做會先遞增base:
if(++base < top) ...
在每種狀況下,它們必須比較這兩個索引來檢查是否會致使雙端隊列爲空。一個非對稱的規則被用來檢查潛在的衝突:pop再次檢查狀態,而且在得到雙端隊列鎖(與take持有的是同一個)時再繼續,當雙端隊列確實爲空時回退。take操做不一樣,它僅僅只是迅速回退,典型的,它接下來會試圖竊取另一個線程的任務。這種非對稱表示是Java與Cilk語言THE協議實現的重要不一樣。
使用volatile型的索引使得push操做只有在雙端隊列數組要溢出時須要同步,此時,它必須首先要獲取雙端隊列鎖來重設數組大小。其餘狀況,需確保在數組元素填充時不受任何take操做影響,而且top被更新。
繼初始實現以後,做者發現有些JVM實現沒有遵照Java內存模型[6],即需確保volatile在寫以後可以讀取實時值。
做爲一種變通方案,要求pop得到鎖後,若是隻有很少於兩個元素時再次嘗試,同時take操做增長另外一個鎖來保證內存邊界。這保證了至多一個索引的改變會被隊列擁有者略過(這樣,對特定平臺也保證了volatile域讀取的及時性),而且只是致使微略的性能損失。
3.2 竊取與空閒
工做竊取框架中的worker線程並不知道它們執行程序的同步要求。它們只是簡單的產生、push、pop、take、管理狀態、執行任務。這些模式的簡單性,確保即便全部線程有不少的任務也能高效的執行。然而,這種流線型化的任務在沒有足夠的任務時的代價依賴於啓發:在啓動一個主任務時,在它完成時,還有在所有中止點時,引入某些fork/join算法。
這裏的主要問題是,當一個工做線程沒有足夠的任務而且又不能從其它線程竊取一個時,它該怎麼辦。當這個程序運行在一個專門化的多核平臺時,它能夠經過依賴於硬件平臺的busy-wait自旋循環來竊取一個。然而,即便是這種狀況,嘗試去竊取一個也增長了衝突,這會致使即使是非空閒線程的執行效率(因爲第3.1節描述的鎖協議)。另外,在這種框架應用的更加典型場景,操做系統會被建議去運行其它不相關任務或線程。
在Java中,確保可以達到這樣的工具是一種弱要求,可是它在實際工做中狀況仍是能夠接受的(相似的技術說明見Hood[3])。一個線程不能從其它線程處竊取任務時,在再次竊取以前會下降它本身的優先級、調用Thread.yield、將它本身在FJTaskRunnerGroup組註冊爲不活躍(inactive)。當全部的worker線程變得不活躍時,它們會阻塞等待主線程的同步。其它狀況,通過給定輪次的自旋,它們會進行睡眠,在再次竊取前,它們會睡眠最多100ms而不是調用yeild方法。這些引入的睡眠會致使那些須要較長的時間來劃分子任務的線程產生人工時延,然而這是最好的通用目的折中。未來版本的框架可能增長附加的控制方法,容許程序員覆蓋默認方法來提升性能。
4. 性能
隨着時間推移,編譯器和JVM不斷被優化,這裏的性能測度數據僅適用於當前。然而,這一節討論的測量狀況揭示了這一框架的基本屬性。
下面表格是七種fork/join測試程序及其簡介的集合。這些程序均修改自util.concurrent包中的示例程序。它們被選取來展現這個框架中可以運行程序的多樣性,同時也爲一些通用的並行測試程序提供測試數據。
程序 | 介紹 |
Fib | 斐波拉契數列,第二節中展現的斐波拉契程序,運行參數47,粒度閾值13 |
Integrate | 高斯正交卷積, (2*i-1)*x(2*i-1)求和,i爲1到5的奇數,積分限-47到48 |
Micro | 一種棋盤最佳移動方式查詢遊戲,查詢後續四步的最佳移動 |
Sort | 快速/歸併排序,(基於Cilk中的一種排序方法),100 000 000個數字 |
MM | 矩陣相乘,2048*2048的double型矩陣 |
LU | LU矩陣分解,4096*4096的double型矩陣 |
Jacobi | 雅可比迭代,4096*4096的double型矩陣,使用網狀調和、最近鄰域平均,迭代上限100 |
主要的測試,在一臺30個CPU的Sun企業版10000路機器,運行着Solaris 7的1.2版本JVM(1.2.2_05版的一個早期版本)。JVM被配置環境變量對每個線程映射「綁定線程」以及在4.2節中討論的內存參數。一些附加的測量報告在4-CPUSun企業版450機器上完成。
爲了最小化計時器和JVM啓動的影響,測試程序運行時的輸入參數很大。其它的一些預加載現象經過在計時器開始前運行一個初始問題集進行避免。大部分數據取了三次運行的中間數,可是有些(接下來4.2~4.4節的大部分測試數據)僅僅取了單次運行數據,所以會有些噪音。
4.1 加速比
可伸縮性測量經過將同一個問題在工做線程數1 ... 30的池中得到。沒辦法得到是否JVM每次都將線程映射到了不一樣的CPU核心,一樣咱們也沒辦法證實這一點。也許將新線程映射到不一樣的CPU隨着線程的增長延遲增長,或者隨着平臺的不一樣在不一樣的的測試程序中不同。然而,常規的,咱們的測試結果顯示,增長線程數可靠的增長了CPU的忙碌程度。
加速比經過Timen/Time1表示。全部程序中,加速比效果最好的是積分程序(30個線程時是28.2),最差的是LU分解程序(30個線程時是15.35)。
另外一種測量可伸縮性指標的依據是任務率,即每一個單一任務執行所須要的平均時間(多是遞歸節點或葉節點)。下面的圖表是採集自同一儀表監測的任務率。明顯的,每單元每線程處理的任務應該是恆定的。事實上,隨着線程數增長,它們略微的下降預示着可伸縮度的限制。須要指出,線程率的普遍差別與各自任務的粒度有關。最小的任務是斐波拉契數列計算,即便閾值設置爲13,在30個線程時每秒能夠執行280萬個任務。
四個因素彷佛能夠解釋在加速曲線的末端出現的降低不是線性的(不是一條直線)。它們中的三個對全部的並行框架都是適用的,可是,有一個對於FJTask是特殊的,即GC影響。
4.2 垃圾回收器
在不少狀況下,現代GC設施能很好的知足fork/join框架:這些程序產生了大量的任務,同時,幾乎它們中的全部任務在執行後迅速的被回收。在任何一個時刻,肯定的fork/join程序須要最多p倍登記內存消耗——相比於這些程序的串行版本。分代的半空間拷貝回收器(包括JVM在[1]中使用的措施,譯者注:Eden代的from和to區是相等的,所以叫「半空間」)可以很好的應對這種狀況,由於它們僅僅遍歷和拷貝沒有被回收的區域。經過這樣作,它屏蔽了在手動並行內存管理中須要處理的一個棘手問題:追蹤那些被一個線程分配,可是被其它線程使用的線程空間。垃圾回收器明顯的不須要知道內存分配的源頭,所以它不須要處理這些問題。
做爲分代拷貝垃圾回收器優點的指示,一個四線程的Fib程序使用了5.1秒。可是,當使用主任務中的內存設置——即禁止內存拷貝時消耗了9.1秒(此時JVM的垃圾回收徹底依賴mark-sweep過程)。然而,當內存分配率太高,線程必須中止來進行回收時這些GC機制會致使可伸縮性問題。上圖顯示了三種內存設置時的加速比(這個JVM支持可選的內存參數設置):默認4M的內存(能夠理解爲eden代的from區)、64M、 2*p+2M內存空間(p爲線程數)。更小的內存時,隨着線程增多,中止線程變得過多,垃圾回收代率上升,垃圾回收器開始影響伸縮性。
爲減小這種影響,全部的測試程序都使用64M內存空間(譯者注:這是足夠大的空間)。一個更好的策略是,在每個測試中,根據線程數進行動態調整(就像圖表中示意的,它讓全部的加速比更趨於線性)。此外,程序的任務粒度閾值能夠隨着線程數的增長而增長。
4.3 內存的局部性與帶寬
四個程序在很是大的數組或矩陣上進行建立和計算測試:數字排序,矩陣相乘、分解、鬆弛迭代。在這些操做裏面,排序多是受這些因素影響最大的,由於它須要在處理器之間移動數據,它受到整個系統內存的聚合帶寬的影響。爲幫助弄清這些影響的實質,排序程序被分紅四個版本:即byte,short,int,long數組。爲確保其它因素一致,每一個版本排序的數據值都在0到255之間。數據位越寬,須要越大的內存流量。測試結果顯示,增長內存流量致使了不好的加速比。雖然,沒有肯定的證據證明,這是致使加速比減少的惟一因素。
元素位長同時也影響絕對性能。好比,單個線程,排序byte數組花費122.5秒,排序long素組花費242.4秒。
4.4 任務同步
如同在3.2節討論的,工做竊取框架有些時候,會遭遇線程間頻繁的全局同步問題。工做線程持續的從隊列中poll任務,雖然此時隊列中沒有。這是就產生了競爭,在FJTask中有時會強制線程空閒睡眠。
Jacobi程序說明了發生的問題。這個程序計算了100步,每一步,全部的單元都依據鄰域平均規則更新。每一步間經過使用全局的柵欄隔離。爲了肯定同步影響的程度,這一程序的一個修改版本每10步才同步一次。尺度的不同顯示了對當前策略的影響,同時指示了這個框架未來須要包含附加的方法以容許程序員覆蓋默認的參數和策略(然而,這張圖也許稍稍彙集了純同步的影響,由於10步同步版本彷佛也保持較大的任務局部性)。
4.5 任務局部性
FJTask與其它的fork/join框架同樣,爲了worker線程消費它們本身建立的大部分線程而優化。當這一點沒有知足時,因爲如下兩個緣由它的性能會受到影響:
(1)竊取任務比從本身的隊列取任務代價更大。
(2)在大部分訪問共享數據的程序中,運行你本身的劃分子任務意味着維持比較好的局部數據訪問性。
從圖表中能夠看出,對於大部分的程序中,竊取任務所佔的比例最多隻是一小部分。然而,隨着工做線程的增長,LU和MM程序產生了大的不平衡工做負載(所以有更多的竊取)。有可能經過算法的調整減少這種影響,從而產生更快的加速比。
4.6 與其它框架的比較
可能進行肯定的意義非凡的不一樣測量,比較不通語言、不一樣框架。然而,相關的指標至少可以顯示FJTask相對與其它語言實現的相似框架的優點和劣勢。下面的表格比較了用Cilk,Hood,Stackthreads,Filaments實現的程序和FJTask框架的性能對比。全部這些程序都運行在4-CPU的Sun企業版450機器(前面已經介紹過)上。爲了避免須要從新配置其它的框架或測試程序,全部的測試運行在相較於上文中更小的問題集。經過編譯器和運行時設置使得程序運行更快,而且全部的結果選擇三次運行中最好的。Fib程序運行時沒有設定粒度閾值,即便用默認的閾值1(在Filaments Fib程序中被設置爲1024,這時它的表現與其它版本的一致).
在一個線程和四個線程的版本中,不一樣框架測試程序的加速比很是接近(在3.0到4.0之間)。然而,對全部這些語言多線程版本的程序更快,所以不一樣點在於不一樣編譯器指定的應用程序屬性、優化開關、可配置參數。實際上,有不少與這裏所用不一樣的選擇,在許多高性能應用中可以產生相似的性能排名。
FJTask在主要進行數組、矩陣浮點計算的程序中一般表現更糟糕(在測試的JVM上)。雖然JVM在不斷的改進,它依然比不過那些有着強大後臺性能優化器的C/C++程序。雖然在這張表中沒有畫出,當編譯優化禁用時,全部這些程序的FJTask版本比其它框架的程序要快。一些非正式的測試顯示,大部分剩下的差別來自於數組邊界檢查和相關的編譯器義務工做。它們顯然時JVM和編譯器開發者應當關心和努力解決的問題。當進行計算密集任務時,代碼質量的不一樣會減小。
5.結論
這篇論文論述了,純粹的Java語言也可以實現可移植、高效可伸縮的處理,而且可以爲程序員提供便捷的API,程序員經過遵循簡單的設計原則和設計模式可以利用這種便利。這裏觀察到的測試結果,不但爲框架的使用者提供了思路,也爲框架自己可能的改進提供了指導。
雖然,可伸縮的測試結果在這裏只是在單JVM上測試,通用的經驗觀察數據值得注意:
分代GC算法一般可以很好適用於並行計算,可是頻繁的GC會影響程序的可伸縮性。在這些JVM上,性能問題的底層緣由是:GC暫停時間幾乎正比於運行線程數。由於更多的運行線程會在單位時間內產生更多的GC,性能開銷甚至攀升到接近線程數目二次方增加。儘管如此,這個明顯的性能影響僅僅在GC率相對較高時纔會出現。這個結果是未來研究和開發並行和併發GC算法的緣由。這裏呈現的結果也附加的說明了,在多處理器JVM上,提供調優選項和自適應機制是可取的。
大部分的可伸縮性測試僅僅只是在更多的CPU上進行,而不是在常規的多處理器上。FJTask(其它的fork/join框架也是)對於2路、4路、8路對稱處理機都提供了一種理想的加速比。這篇論文彷佛是第一篇在多於16處理器上進行的系統的常規多處理機fork/join框架報告。這裏的結果在其它平臺是否依然有效須要後續的測量。
應用程序的特色(包括內存局部性,任務局部性,使用所有同步)一般更須要權衡可伸縮度、絕對性能而不是框架特色、JVM或者底層OS。好比,非正式的測試顯示,在雙端隊列中當心的避免全局同步在低任務產生率的程序中(好比LU)對性能幾乎沒有影響。然而,專一於保持任務小開銷讓它有更普遍的適用度,利用框架和相關的設計是一項編程技術。
除了持續的改進,未來在這個框架上的努力包括:構建有用的應用程序(與演示程序的對照和測試),在生產環境壓力下的評估,在不一樣的JVM實現上的測試,爲羣集多處理器進行擴展開發等。
6. 致謝(譯者:論文翻譯至此告一段落,爲確保完整性,後續部分將直接附上原文)
This work was supported in part by a collaborative research grant from Sun Labs. Thanks to Ole Agesen, Dave Detlefs, Christine Flood, Alex Garthwaite, and Steve Heller of the Sun Labs Java Topics Group for advice, help, and comments. David Holmes, Ole Agesen, Keith Randall, Kenjiro Taura, and the anonymous referees provided useful comments on drafts of this paper. Bill Pugh pointed out the read−after−write limitations of JVMs discussed in section 3.1. Very special thanks to Dave Dice for reserving time and performing test runs on the 30−way Enterprise.
7. 參考文獻[1] Agesen, Ole, David Detlefs, and J. Eliot B. Moss. Garbage Collection and Local Variable Type−Precision andLiveness in Java Virtual Machines. In Proceedings of 1998 ACM SIGPLAN Conference on Programming Language Design and Implementation (PLDI), 1998.[2] Agesen, Ole, David Detlefs, Alex Garthwaite, Ross Knippel, Y.S. Ramakrishna, and Derek White. An Efficient Meta−lock for Implementing Ubiquitous Synchronization. In Proceedings of OOPSLA ’99, ACM, 1999.[3] Arora, Nimar, Robert D. Blumofe, and C. Greg Plaxton. Thread Scheduling for Multiprogrammed Multiprocessors. In Proceedings of the Tenth Annual ACM Symposium on Parallel Algorithms and Architectures (SPAA), Puerto Vallarta, Mexico, June 28 − July 2, 1998.[4] Blumofe, Robert D. and Dionisios Papadopoulos. Hood: A User−Level Threads Library for Multiprogrammed Multiprocessors. Technical Report, University of Texas at Austin, 1999.[5] Frigo, Matteo, Charles Leiserson, and Keith Randall. The Implementation of the Cilk−5 Multithreaded Language. In Proceedings of 1998 ACM SIGPLAN Conference on Programming Language Design and Implementation (PLDI), 1998.[6] Gosling, James, Bill Joy, and Guy Steele. The Java Language Specification, Addison−Wesley, 1996.[7] Lea, Doug. Concurrent Programming in Java, second edition, Addison−Wesley, 1999.[8] Lowenthal, David K., Vincent W. Freeh, and Gregory R. Andrews. Efficient Fine−Grain Parallelism on Shared−Memory Machines. Concurrency−Practice and Experience, 10,3:157−173, 1998.[9] Simpson, David, and F. Warren Burton. Space efficient execution of deterministic parallel programs. IEEE Transactions on Software Engineering, December, 1999.[10] Taura, Kenjiro, Kunio Tabata, and Akinori Yonezawa. "Stackthreads/MP: Integrating Futures into Calling Standards." In Proceedings of ACM SIGPLAN Symposium on Principles & Practice of Parallel Programming (PPoPP), 1999.