PLINQ并行開(kāi)發(fā)中常見(jiàn)性能問(wèn)題及應(yīng)對(duì)方案
在上月舉行的PDC 09大會(huì)上,微軟并行庫(kù)團(tuán)隊(duì)的開(kāi)發(fā)工程師Igor Ostrovsky介紹了PLINQ的工作原理,以及多核編程中,尤其是在PLINQ使用過(guò)程中幾種常見(jiàn)性能問(wèn)題及應(yīng)對(duì)方法。Igor表示,這些性能問(wèn)題很少在順序編程中遇到,因此在并行環(huán)境中容易被人忽視。
#T#
第一個(gè)性能問(wèn)題是內(nèi)存分配
由于利用了多核CPU進(jìn)行運(yùn)算,對(duì)象分配的速度也加快了。此外,程序中可以還會(huì)出現(xiàn)更高頻率的字符串連接或裝箱操作,這都會(huì)使GC壓力增大。.NET應(yīng)用程序所使用的默認(rèn)GC方式為Concurrent GC,它的性能很高,并且為降低應(yīng)用程序的延遲作了很多優(yōu)化。它的最佳使用場(chǎng)景是用戶(hù)交互式應(yīng)用,這樣可以盡可能避免用戶(hù)界面的停頓,但是它在長(zhǎng)期運(yùn)行的多核程序中表現(xiàn)并不好。而最終的結(jié)果是大量計(jì)算時(shí)間耗費(fèi)在GC上,此時(shí)應(yīng)用程序算法即便是利用了多個(gè)核,也會(huì)發(fā)現(xiàn)它的伸縮能力受到了GC限制。解決這個(gè)問(wèn)題的方法之一是減小內(nèi)存分配,例如可以使用值類(lèi)型來(lái)代替引用類(lèi)型。值類(lèi)型的對(duì)象會(huì)分配在線程棧而不是堆上,以此避免對(duì)GC產(chǎn)生壓力。第二個(gè)方法是在config文件中啟用Server GC。使用Server GC會(huì)改變.NET分配對(duì)象的方式,此時(shí).NET會(huì)為每個(gè)核準(zhǔn)備不同的堆,并且獨(dú)立進(jìn)行垃圾回收。這樣在一臺(tái)4核的機(jī)器上便可以有4個(gè)線程同時(shí)進(jìn)行垃圾回收,性能自然也就隨著多核而提升了。
第二個(gè)性能問(wèn)題是CPU在局部化(Locality)和緩存方面的問(wèn)題
在流行的多核架構(gòu)中,每個(gè)核都有獨(dú)立的二級(jí)緩存。CPU并不會(huì)緩存單個(gè)地址中的數(shù)據(jù),而是緩存以64字節(jié)或128字節(jié)相鄰內(nèi)存的緩存條目(cache line),因此當(dāng)某個(gè)核改變了內(nèi)存中的數(shù)據(jù)時(shí),則其他核中地址相鄰的緩存數(shù)據(jù)也會(huì)失效,這樣CPU每次進(jìn)行計(jì)算時(shí)都要從速度較慢的內(nèi)存中加載數(shù)據(jù)。這個(gè)性能問(wèn)題的隱蔽之處在于代碼中的不同數(shù)據(jù)——例如同一個(gè)數(shù)組的不同下標(biāo)——可能在內(nèi)存中處在同一個(gè)緩存條目中,因此這個(gè)問(wèn)題又被稱(chēng)為錯(cuò)誤共享(False Sharing)。Igor演示了一段性能低下的代碼,在這個(gè)實(shí)現(xiàn)中多個(gè)線程會(huì)不斷讀寫(xiě)同一個(gè)數(shù)組的相鄰下標(biāo),因此造成了錯(cuò)誤共享。Igor的修改方法是將數(shù)據(jù)存放在數(shù)組中相距較遠(yuǎn)的下標(biāo),甚至是不同的數(shù)組中。由于CPU的緩存條目大小有限,這種方法可以避免出現(xiàn)錯(cuò)誤共享。博客園老趙在《計(jì)算機(jī)體系結(jié)構(gòu)與程序性能》一文中也提出了一種優(yōu)化方式,他的做法是盡可能使用局部變量來(lái)保存計(jì)算過(guò)程中的中間值,以此減少對(duì)數(shù)組的修改操作。由于局部變量分處不同線程的棧空間內(nèi),因此地址相距很遠(yuǎn),不會(huì)造成錯(cuò)誤共享問(wèn)題。當(dāng)有人問(wèn)起到這種優(yōu)化方式是否安全時(shí),Igor答到,這其實(shí)和CPU架構(gòu)的實(shí)現(xiàn)方式有很大關(guān)系。如果某一天緩存實(shí)現(xiàn)變化了,可能這種優(yōu)化方式會(huì)適得其反。不過(guò)在目前主流架構(gòu)中,這樣的做法是比較安全的。Igor補(bǔ)充道,他認(rèn)為這也是為什么“全自動(dòng)”并行化那么困難的原因之一,因?yàn)樵诓⑿协h(huán)境下影響程序性能的方面實(shí)在太多了。
第三個(gè)問(wèn)題在于開(kāi)發(fā)人員傾向于在PLINQ中使用大量小粒度的委托來(lái)完成工作
此時(shí)每個(gè)委托的計(jì)算任務(wù)很小,而委托的執(zhí)行次數(shù)會(huì)很多。在計(jì)算較長(zhǎng)的序列時(shí),小粒度的委托對(duì)象也能獲得性能提高,但是它會(huì)產(chǎn)生額外的負(fù)載。例如,MoveNext和Current的調(diào)用,以及每個(gè)委托的執(zhí)行性能都和虛方法比較接近。此外,一個(gè)較長(zhǎng)的輸入序列也會(huì)受限于內(nèi)存的吞吐量。因此,Igor建議開(kāi)發(fā)人員在使用PLINQ時(shí)盡可能使用計(jì)算量較大的委托,以此減少計(jì)算主體外的性能開(kāi)銷(xiāo)。
第四和第五問(wèn)題則與PLINQ的實(shí)現(xiàn)有關(guān)
Igor表示,PLINQ可以并行執(zhí)行所有的LINQ查詢(xún),但是相對(duì)于復(fù)雜的LINQ查詢(xún),PLINQ能夠?qū)?jiǎn)單的LINQ操作有更好的優(yōu)化。因此,Igor建議開(kāi)發(fā)人員在使用PLINQ時(shí)可以手動(dòng)將復(fù)雜的LINQ表達(dá)式拆分為簡(jiǎn)單的LINQ查詢(xún),并且只在真正需要大量計(jì)算的地方才開(kāi)始并行化。這種結(jié)合順序執(zhí)行和并行執(zhí)行的方式,可以讓?xiě)?yīng)用程序的性能達(dá)到最優(yōu)。此外,為不同的輸入方式選擇不同的分塊(partition)策略對(duì)性能的影響很大,因此PLINQ會(huì)對(duì)數(shù)組和IList<>進(jìn)行靜態(tài)的分割,而對(duì)IEnumerable<>集合按實(shí)際需求進(jìn)行劃分,而開(kāi)發(fā)人員也可以通過(guò)自定義Partitioner的方式來(lái)指定特別的分割策略。
最后,Igor強(qiáng)調(diào),使用并行計(jì)算進(jìn)行程序性能優(yōu)化之前,一定要通過(guò)合適的評(píng)測(cè)方式來(lái)找到代碼的瓶頸。如果這個(gè)瓶頸正符合數(shù)據(jù)并行(data parallel)模式,那么可以使用PLINQ進(jìn)行性能優(yōu)化。而優(yōu)化完成后還需要評(píng)測(cè)其效果,并使用之前提出的幾種方案進(jìn)行合適的調(diào)整。

延伸閱讀
PLINQ(Parallel LINQ)。微軟對(duì)PLINQ在Parallel FX中的定位是:PLINQ是TPL(Task Parallel Library)的一個(gè)高層應(yīng)用。目前PLINQ已經(jīng)被集成到.NET 4.0當(dāng)中了。


























