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

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
