2001年,IBM推出了基于雙核的Power4處理器;隨后Sun和HP都先后推出了基于雙核架構(gòu)的UltraSPARC IV以及PA-RISC8800處理器。但這些面向高端應(yīng)用的RISC處理器曲高和寡,并沒有能夠引起廣大群眾的關(guān)注。直到2005年第二季度,Intel發(fā)布了基于X86的桌面雙核處理器,從此多核才走進(jìn)平常百姓家。 在今天多核處理器已占 據(jù)了越來越多的市場份額,作為一線的編程人員,我們必須直面多核革命帶來的沖擊。多核編程,既是機(jī)遇也是挑戰(zhàn),如何在這個行業(yè)大變革中把握方向、與時俱 進(jìn),成為擺在我們面前的迫切課題。因?yàn)閺膯魏说蕉嗪瞬⒉幌裉幚砥鲿r鐘頻率的提升那樣對程序員而言是透明的,如果我們的編寫的程序沒有針對多核的特點(diǎn)來設(shè)計(jì),那就不能完全獲得多核帶來的性能提升。在這個新舊交替的戰(zhàn)國時代,我們有什么選擇、能否借鑒以前的開發(fā)經(jīng)驗(yàn)? 是的,人類最為偉大的技能就是能夠借鑒之前的經(jīng)驗(yàn)。我們應(yīng)該借鑒前人的經(jīng)驗(yàn),積極學(xué)習(xí)并行編程技能同時在實(shí)際工作中小心求證、謹(jǐn)慎行動。多核,特別是雙核,與雙路SMP(對稱多處理器)架構(gòu)非常相似: 圖 1 Intel和AMD的雙核CPU結(jié)構(gòu)示意圖 從圖 1可以看到盡管Intel與AMD的雙核技術(shù)有所不同,但仍然可以發(fā)現(xiàn)所謂雙核處理器就是將兩個運(yùn)算核心集成在一個處理器上。這跟在一塊主板上集成兩顆處理器的雙路SMP系統(tǒng)相當(dāng)相似,不同之處僅在于雙核系統(tǒng)兩個計(jì)算核心之間相互交換數(shù)據(jù)并不需要通過前端系統(tǒng)總線(FSB),而雙路系統(tǒng)的兩個處理器是通過FSB來交換數(shù)據(jù)的,這也是我們編寫程序時需要注意的一個小細(xì)節(jié)。 就像針對SMP編程一樣,針對多核處理器編程也必須使用多線程或者多進(jìn)程的形式來編寫應(yīng)用程序才能夠得到多核帶來的性能提升??梢?span style="color: blue;">我們在SMP并行編程上積累的經(jīng)驗(yàn)大多都可以應(yīng)用到多核編程上來。
編程的變革多核時代的到來,給我們的編程思維帶來了巨大的沖擊。為了能夠充分地利用多核性能,我們必須學(xué)會以分塊的思維設(shè)計(jì)程序、以多進(jìn)程或多線程的形式來編寫程序。到底應(yīng)該使用多進(jìn)程還是多線程的形式來編寫程序是最讓程序員感到困惑的問題之一,我覺得需要根據(jù)具體的應(yīng)用來決定;但通常情況下使用多線程進(jìn)行多核編程比使用多進(jìn)程有更大的優(yōu)勢: A) 線程的創(chuàng)建和切換開銷比進(jìn)程更小。 B) 線程間通信的方式多而且簡單也更有效率。 C) 多線程有汗牛充棟的基礎(chǔ)庫支持。 D) 多線程的程序比多進(jìn)程的程序更容易理解和修改。 除了編程形式,我們使用多線程編程的動機(jī)也發(fā)生了改變。在以往,對于Windows程序員來說,使用多線程的主要原因之一是為了提高用戶體驗(yàn):如在長時間的計(jì)算中提高UI、I/O或者網(wǎng)絡(luò)的響應(yīng)速度。而在多核時代我們編寫應(yīng)用程序?yàn)榱顺浞掷枚鄠€計(jì)算核心,縮短計(jì)算時間或者在相同的時間段內(nèi)計(jì)算更多任務(wù)。如在游戲編程時通過多線程的方式把碰撞檢測的計(jì)算分散到多個CPU內(nèi)核可以大大縮減計(jì)算時間;也可以利用多核做更細(xì)致的檢測計(jì)算,從而能夠模擬更加真實(shí)的碰撞。 在多核時代,我們對編程語言的選擇也要更加謹(jǐn)慎。這一小節(jié)的內(nèi)容雖然是個人見解但的確值得系統(tǒng)開發(fā)、游戲開發(fā)甚至Web開發(fā)程序員一起探討。無論開發(fā)何種項(xiàng)目,相對于C/C++/Fortran等編譯型語言,C#/java/Python等腳本語言也許是更好的選擇。原因在于腳本語言比較高級,一般都提供了對多線程的原生支持;如C#的System.Threading.Thread、java的java.lang.Thread及Python的Threading.Thread。相形之下,編譯型語言往往都是通過平臺相關(guān)的庫來提供多線程支持,如Win32 SDK、POSIX threads等。沒有統(tǒng)一的標(biāo)準(zhǔn),造成使用C/C++編寫多線程程序需要考慮更多的細(xì)節(jié),提高了項(xiàng)目成本。從現(xiàn)在來看,C/C++的用戶雖然不少,但在多核時代腳本語言會更受歡迎,因?yàn)榇『谜{(diào)頭啊,腳本語言一般都沒有ISO標(biāo)準(zhǔn),說改就可以改,很快就會出現(xiàn)針對多核的解釋器和編譯器了。不過PHP/Ruby/Lua等腳本語言就會比較難得到多核程序員們的寵愛了——因?yàn)樗鼈儾]有提供內(nèi)核級線程支持,它們的多線程是用戶級的甚至不支持線程,用它們編寫的多線程程序仍然無法完全利用多核優(yōu)勢。 表 1 各種語言對多線程支持的比較
雖然C/C++在多線程編程方面因?yàn)闆]有從語言級提供支持而失去了部分優(yōu)勢;但因?yàn)楫?dāng)前的主流操作系統(tǒng)都以C語言接口的方式提供創(chuàng)建線程的API,而C/C++又有相當(dāng)豐富的程序庫,也就一定程度上彌補(bǔ)了語言上的不足。使用C/C++編寫多線程程序不僅可以使用Win32 SDK,還可以使用POSIX threads、MFC和boost.thread等。雖然這些庫都提供了一定程度的封裝,減輕了程序員進(jìn)行多線程負(fù)擔(dān),但對于目標(biāo)定位于提升計(jì)算密集型程序的性能的多核程序員來說,這些方式仍然太為復(fù)雜。因?yàn)槭褂眠@些庫幾乎要增加一倍的關(guān)鍵代碼,相應(yīng)地調(diào)試和測試的成本也大大增加。更好的選擇應(yīng)該是使用OpenMP這種通過編譯器加強(qiáng)來支持多線程的基礎(chǔ)庫。OpenMP通過使用#pragma編譯器指令來指定并行代碼段,對程序的改動相當(dāng)少;而且可以指定編譯為串行版本以方便調(diào)試,更可以和不支持OpenMP的編譯器共存。 可見即便腳本語言在語言層次上提供了對多線程編程的原生支持,但卻并沒有比C/C++領(lǐng)先多遠(yuǎn)。根本原因在于腳本語言的基礎(chǔ)——數(shù)據(jù)結(jié)構(gòu)與算法的基礎(chǔ)庫與CRT/STL等C/C++基礎(chǔ)庫然一樣是以串行形式來設(shè)計(jì)開發(fā)的。針對多核編程去修改基礎(chǔ)庫這一幾乎所有編程語言都需要面對的燃眉之急是拉開兩大陣營領(lǐng)先優(yōu)勢的生死之戰(zhàn),而所有權(quán)集中于某一公司或者組織的C#/java/Python這類腳本語言船小好調(diào)頭,估計(jì)將贏得這場關(guān)鍵之役。這就是我在上文推薦選擇使用腳本來編寫程序的原因之一。
多核程序設(shè)計(jì)隨著時間推進(jìn),我們終將需要面對多核系統(tǒng)來設(shè)計(jì)程序。多核編程我個人認(rèn)為基本上等同于共享內(nèi)存的并行編程,多核程序設(shè)計(jì)可以借鑒以往并行編程的經(jīng)驗(yàn)——如分塊的設(shè)計(jì)思維、并行設(shè)計(jì)方法論和多樣的并行支持方式。 首先我們來談?wù)劮謮K的設(shè)計(jì)思維。因?yàn)榫€程是操作系統(tǒng)分配CPU資源的最小單位,所以如果想要設(shè)計(jì)多核并行的程序,那么我們就要形成將程序分塊的設(shè)計(jì)思維。還記得初中課本上 比如,想泡壺茶喝。當(dāng)時的情況是:開水沒有;水壺要洗,茶壺茶杯要洗;火生了,茶葉也有了。怎么辦? 辦法甲:洗好水壺,灌上涼水,放在火上;在等待水開的時間里,洗茶壺、洗茶杯、拿茶葉;等水開了,泡茶喝。 辦法乙:先做好一些準(zhǔn)備工作,洗水壺,洗茶壺茶杯,拿茶葉;一切就緒,灌水燒水;坐待水開了泡茶喝。 辦法丙:洗凈水壺,灌上涼水,放在火上,坐待水開;水開了之后,急急忙忙找茶葉,洗茶壺茶杯,泡茶喝。 哪一種辦法省時間?我們能一眼看出第一種辦法好,后兩種辦法都窩了工。 假定華老有兩個機(jī)器人給他泡茶喝,那最好的方法顯然是按照“辦法甲”分工:機(jī)器人A去燒水,機(jī)器人B洗茶具;等水開了,泡茶喝???,不經(jīng)意間,我們就應(yīng)用了分塊的思維——把不相關(guān)的事務(wù)分開給不同的處理器執(zhí)行。再舉個我們工作中經(jīng)常遇到的例子:有數(shù)據(jù)類型為T的序列A,求序列中值與K相等的元素個數(shù)。實(shí)現(xiàn)這個功能的C++函數(shù)如下: 代碼 1 統(tǒng)計(jì)序列中值為K的元素個數(shù)
從代碼 1 統(tǒng)計(jì)序列中值為K的元素個數(shù)中顯而易見Count(k, p, n) = Count(k, p, n/2) + Count(k, p+n/2, n-n/2),即序列中值等于K的元素個數(shù)為前半段中值為K的元素個數(shù)加上后半段中值等于K的元素個數(shù)。如果我們開啟兩條線程,一條統(tǒng)計(jì)前半段(執(zhí)行Count(k, p, n/2)),另一條統(tǒng)計(jì)后半段(執(zhí)行Count(k, p+n/2, n-n/2)),那么在雙核系統(tǒng)上我們將可以節(jié)省一半的運(yùn)行時間(忽略生成線程的開銷等)。 以上分塊的思維都是簡單直接的,如果是復(fù)雜的任務(wù),就不可能容易地找出分塊的方案了,所以需要并行設(shè)計(jì)的方法論來指導(dǎo)我們。經(jīng)過幾十年的并行程序研究,前人已經(jīng)總結(jié)出若干行之有效的并行設(shè)計(jì)方法,在這里介紹一個經(jīng)典的方法:數(shù)據(jù)相關(guān)圖。仍然以《統(tǒng)籌方法》中經(jīng)典的泡茶為例,我們可以畫出以下數(shù)據(jù)相關(guān)圖:
圖 2 《統(tǒng)籌方法》中辦法甲的數(shù)據(jù)相關(guān)圖 從圖 2 《統(tǒng)籌方法》中辦法甲的數(shù)據(jù)相關(guān)圖中可以看出數(shù)據(jù)相關(guān)圖是一個有向圖,其中每個頂點(diǎn)代表一個要完成的任務(wù);箭頭表示箭頭指向的任務(wù)依賴于引出箭頭的任務(wù),如果數(shù)據(jù)相關(guān)圖中沒有從一個任務(wù)到另一個任務(wù)的路徑,那么這兩個任務(wù)不相關(guān),可以并行處理。如果華老自己動手泡茶喝,那圖 2 《統(tǒng)籌方法》中辦法甲的數(shù)據(jù)相關(guān)圖中紅色虛框的部分是可以并行的;而如果華老有兩個機(jī)器人幫他泡茶,而且有不少于2個水龍頭供機(jī)器人使用,那綠色虛框的部分都可以并行而且能取得更高的效率。可見能夠合理利用的資源越多,并行的加速比率就越高。 在數(shù)據(jù)相關(guān)圖中,如果有不相關(guān)的任務(wù)對數(shù)據(jù)集的不同元素進(jìn)行相同的操作,我們稱這種數(shù)據(jù)相關(guān)表現(xiàn)了數(shù)據(jù)并行性。如在科學(xué)計(jì)算中經(jīng)常會對某一N維向量乘以一個實(shí)數(shù)值: for( int i = 0; i < N; ++i) v[i] *= r; 如果有N個處理器,那么這N次帶有數(shù)據(jù)并行性的迭代可以同時執(zhí)行。除了數(shù)據(jù)并行性,如果有不相關(guān)的任務(wù)對數(shù)據(jù)集的不同元素進(jìn)行不同的操作,則表現(xiàn)了功能并行性。還有形狀為簡單路徑或鏈的數(shù)據(jù)相關(guān)圖意味著在處理單個問題上不存在并行性,但如果需要處理多個問題,且每個問題可以分為幾個階段,那么就能支持與階段數(shù)相同的并行性,這種情況稱之為流水線。關(guān)于功能并行性和流水線,由于篇幅關(guān)系,這里不能詳述,有興趣的讀者可以查閱并行編程相關(guān)書籍。 既有了分塊的思維,又有并行程序設(shè)計(jì)的方法論作為指引,現(xiàn)在我們就缺怎么去開發(fā)并行程序了。當(dāng)前比較流行的思想有以下三種: 一、擴(kuò)展編譯器。開發(fā)并行化編譯器,使其能夠發(fā)現(xiàn)和表達(dá)現(xiàn)有串行語言程序中的并行性,例如Intel C++ Compiler就有自動并行循環(huán)和向量化數(shù)據(jù)操作的功能。這種把并行化的工作留給編譯器的方法雖然降低了編寫并行程序的成本,但因?yàn)檠h(huán)和分枝等控制語句的復(fù)雜組合,編譯器不能識別相當(dāng)多的可并行代碼而錯誤地編譯成了串行版本。 二、擴(kuò)展串行編程語言。這是當(dāng)前最為流行的方式,通過增加函數(shù)調(diào)用或者編譯指令來表示低層語言以獲取并行程序。用戶能夠創(chuàng)建和結(jié)束并行進(jìn)程或線程,并提供同步與通信的功能函數(shù)等。這方面較為杰出的庫有MPI和OpenMP等;在解釋型腳本陣營,ParallelPython也吸引了不少粉絲。 三、創(chuàng)造一個并行語言。雖然這是一個瘋狂的想法,但事實(shí)上近幾十年來一直有人在做這樣的事情,如HPF(High Performance Fortran)是Fortran90的擴(kuò)展,用多種方式支持?jǐn)?shù)據(jù)并行程序。 在以后的多核編程之旅中,我們將會發(fā)現(xiàn)上面所述只是滄海一粟,并行計(jì)算領(lǐng)域有著更多的知識值得我們學(xué)習(xí),也有更廣闊的空間給我們實(shí)現(xiàn)自己的想法。
新瓶舊酒雖然多核CPU正在成為主流,但畢竟時間不長,現(xiàn)在大部分應(yīng)用程序都是在單核時代開發(fā)的,那么這些舊程序如何才能在新的環(huán)境煥發(fā)自己新的光彩?在這里我給出自己的幾點(diǎn)見解: 1) 精確地評估舊程序是否需要作出修改。如Foxmail、Windows優(yōu)化大師之類的桌面軟件原本就只占用極少的CPU資源,那么就不需要針對多核改寫。而作為網(wǎng)站服務(wù)器端運(yùn)行的CGI程序基本上都是以多進(jìn)程或多線程的方式來響應(yīng)請求的,將可以平滑地充分利用多核系統(tǒng)的性能優(yōu)勢,一般而言也不需要針對多核改寫。 2) 就重避輕。一個應(yīng)用程序,性能瓶頸通常都只有幾個或者一兩個甚至這些瓶頸相關(guān)的功能還是用戶很少使用的。那么為了這些少量需求而對已有程序進(jìn)行傷筯動骨的改造是不合適的,更不宜以多線程的架構(gòu)重寫整個應(yīng)用。如果應(yīng)用程序是使用C/C++/Fortran編寫的,那使用OpenMP使性能瓶頸部分的代碼并行化是相當(dāng)好的選擇。如果代碼是使用C#/java/Python等腳本編寫的,可能需要多花一些功夫來完成同樣的工作。 3) 不追逐潮流。一句話,如果舊的應(yīng)用程序沒有性能瓶頸,那就不要作任何改動,否則只會引火燒身。像暴風(fēng)影音、千千靜聽這一類多媒體播放軟件,針對多核優(yōu)化是可做可不做的事情;但如果做了,用戶可能反而會覺得太占用資源,因?yàn)閾Q了雙核系統(tǒng)還覺得播放視頻/音頻的時候做其它事情仍然有點(diǎn)“卡”,那就不如不做。 綜上所述,如果我們手上維護(hù)著舊的程序,那我們最應(yīng)該做的事情是評估軟件是否有性能瓶頸,切忌為雙核而雙核,要以不變應(yīng)萬變。
寫在最后多核時代的到來,必定會給編程帶來巨大的變化,對此我有幾個建議: 一、 并行計(jì)算方面已經(jīng)有相當(dāng)多的研究人員作了幾十年的基礎(chǔ)工作,有相當(dāng)多可以學(xué)習(xí)和利用的知識。我們應(yīng)該學(xué)復(fù)雜的、用簡單的,復(fù)雜如MPI也要去了解了解,但應(yīng)用的時候就越簡單越好,如上文代碼 1 統(tǒng)計(jì)序列中值為K的元素個數(shù)的函數(shù)比較好的并行方案是使用OpenMP: 代碼 2 使用OpenMP并行
簡單地增加了一行源碼就實(shí)現(xiàn)了并行,不僅比使用Win32 SDK/PThreads創(chuàng)建線程的代碼少得多而且更容易維護(hù)。 二、 如非必要,不要并行。一 直以來,我們都是接受串行編程的教育,而且大多數(shù)程序員都習(xí)慣于編寫串行程序。即使我們對并行編程進(jìn)行了學(xué)習(xí),實(shí)踐的時候仍然難免會引起一堆讓人手忙腳亂 的麻煩。所以現(xiàn)階段在實(shí)際項(xiàng)目中如非必要,不要并行;比較適宜的方式是先在非核心業(yè)務(wù)中熟悉并行編程,然后再在有必要性的部分工作中實(shí)操。 三、 并行可以作為最后的優(yōu)化手段。知道在什么時候使用并行跟知道如何編寫并行代碼一樣重要。如果你竭盡全力優(yōu)化之后程序仍然不能讓你的老板、客戶滿意,那你可以試試將性能瓶頸部分并行化,作為優(yōu)化的最后選擇。
參考資料書籍類: ·Micheal J. Quinn著,陳文光 等譯 《MPI與OpenMP并行程序設(shè)計(jì)C語言版》 清華大學(xué)出版社 2004年10月 ·David R. Butenhof 著,于磊 等譯 《POSIX多線程程序設(shè)計(jì)》 中國電力出版社 2003年4月 ·Jim Beveridge等著,侯捷 譯 《Win32多線程程序設(shè)計(jì)》 華中科技大學(xué)出版社 2002年1月 互聯(lián)網(wǎng)資料類: ·《統(tǒng)籌方法》http://hi.baidu.com/civay/blog/item/0bbcb016464e4351f2de3239.html ·《從奔騰D到安騰2 英特爾雙核處理器歷史》http://bbs./thread-388811-1-39.html ·《什么是雙核處理器》http://bbs.zol.com.cn/index20060307/index_17_242321.html
作者簡介賴勇浩,男,現(xiàn)供職于網(wǎng)易廣州。平時較關(guān)注多核編程、GameAI和最優(yōu)化計(jì)算等,對C++/Python有一定了解。業(yè)余喜歡為個人博客http://blog.csdn.net/lanphaday撰寫編程心得與大家分享。可以通過Email:lanphaday@126.com和我聯(lián)系。 |
|