算入門:從搬磚比喻到多核編程實(shí)戰(zhàn))
簡(jiǎn)介這份資源是天津大學(xué)并行計(jì)算課程的配套學(xué)習(xí)資料面向計(jì)算機(jī)專業(yè)學(xué)生、高性能計(jì)算方向的學(xué)習(xí)者以及需要系統(tǒng)梳理并行編程知識(shí)的技術(shù)人員幫助其理解共享內(nèi)存與分布式內(nèi)存模型、掌握OpenMP與MPI等并行編程工具并具備將串行算法改造為并行程序的能力。壓縮包為zip格式整體約44.18MB文件總數(shù)與類型明細(xì)上游暫未提供從內(nèi)容看應(yīng)包含課程講義、實(shí)驗(yàn)報(bào)告與復(fù)習(xí)總結(jié)等材料。目前已有1587人學(xué)習(xí)具備一定的參考價(jià)值。資料圍繞并行計(jì)算基礎(chǔ)、負(fù)載均衡、同步與互斥、分治與歸約掃描等并行算法設(shè)計(jì)策略展開并結(jié)合Amdahl定律與Gustafson定律進(jìn)行性能分析實(shí)驗(yàn)部分涉及并行排序、矩陣運(yùn)算、圖形渲染等場(chǎng)景復(fù)習(xí)總結(jié)則梳理考試重點(diǎn)與解題思路便于讀者對(duì)照實(shí)驗(yàn)代碼鞏固理論、查漏補(bǔ)缺。1. 并行計(jì)算到底在算什么從“一個(gè)人搬磚”到“一群人搬磚”的直覺你第一次聽到“并行計(jì)算”這四個(gè)字大概率是在某門課、某個(gè)項(xiàng)目介紹或者一份資料標(biāo)題里。它聽起來很唬人像是只有超級(jí)計(jì)算機(jī)才配談的東西。但說白了它解決的是一個(gè)特別樸素的問題活兒太多一個(gè)人干不完怎么辦打個(gè)比方。你要把一萬塊磚從樓下搬到樓上。一個(gè)人搬來回一萬趟累死也得好幾天。但如果你叫來十個(gè)人每人搬一千塊時(shí)間立刻縮到十分之一。并行計(jì)算干的就是這件事——只不過搬的不是磚是計(jì)算任務(wù)叫來的不是人是 CPU 核心、GPU 線程或者一整個(gè)計(jì)算集群。它的核心思想就一句話把一個(gè)大任務(wù)拆成許多能同時(shí)干的小任務(wù)讓多個(gè)計(jì)算單元一起動(dòng)手從而縮短總時(shí)間。為什么值得搞懂因?yàn)榻裉炷阌玫氖謾C(jī)、電腦、服務(wù)器幾乎全是多核的。單核性能早就撞上了物理天花板廠商拼的是“核多不多、能不能協(xié)同”。不懂并行你寫出來的程序可能只用到一個(gè)核剩下七個(gè)核在旁邊看戲——這就是很多“玄學(xué)卡頓”的根源。并行計(jì)算不是什么高冷概念它是理解現(xiàn)代硬件如何真正干活的一把鑰匙。2. 為什么非要并行單核撞墻之后算力只能靠“人多”2.1 頻率上不去了核數(shù)成了新賽道早年間提升算力很簡(jiǎn)單把 CPU 主頻往上拉就行。但從十幾年前開始這條路走不動(dòng)了。主頻越高發(fā)熱和功耗越夸張散熱壓不住芯片也扛不住。工程師們換了個(gè)思路既然單個(gè)核心快不了那就多放幾個(gè)核心讓它們同時(shí)干活。于是雙核、四核、八核乃至幾十核的處理器成了常態(tài)。這帶來一個(gè)直接后果硬件已經(jīng)把“人多”的條件給你了但軟件得跟得上。一個(gè)只按“一個(gè)人搬磚”思路寫的程序放到八核機(jī)器上依然只有一個(gè)核在忙。并行計(jì)算就是教你怎么把任務(wù)分出去讓多出來的核真正派上用場(chǎng)。換句話說并行不是可選項(xiàng)而是榨干現(xiàn)代硬件性能的必經(jīng)之路。2.2 兩種“人多”的方式多進(jìn)程與多線程讓多個(gè)計(jì)算單元同時(shí)干活常見的有兩條路。一條叫多進(jìn)程每個(gè)進(jìn)程是獨(dú)立的“工人”各有各的內(nèi)存空間互不干擾適合把大任務(wù)切成幾塊互不依賴的子任務(wù)。另一條叫多線程多個(gè)線程共享同一塊內(nèi)存像同一個(gè)車間里的幾個(gè)工人共用一張工作臺(tái)溝通方便但也容易互相踩腳。用 Python 寫個(gè)最直觀的對(duì)比感受一下“串行”和“并行”的差別import time from multiprocessing import Pool def heavy_task(n): # 模擬一段耗時(shí)的計(jì)算比如累加 total 0 for i in range(n): total i * i return total if __name__ __main__: tasks [10_000_000] * 4 # 四個(gè)相同的重任務(wù) # 串行一個(gè)接一個(gè)地算 start time.time() serial_result [heavy_task(t) for t in tasks] print(串行耗時(shí):, time.time() - start) # 并行開四個(gè)進(jìn)程同時(shí)算 start time.time() with Pool(processes4) as pool: parallel_result pool.map(heavy_task, tasks) print(并行耗時(shí):, time.time() - start)這段代碼里Pool(processes4)就是叫來四個(gè)“工人”pool.map把四個(gè)任務(wù)分給它們同時(shí)干。參數(shù)processes控制工人數(shù)量一般設(shè)成 CPU 核心數(shù)比較合適。跑下來你會(huì)發(fā)現(xiàn)并行耗時(shí)明顯更短——這就是并行的直接收益。當(dāng)然真實(shí)場(chǎng)景遠(yuǎn)比這復(fù)雜任務(wù)之間可能有依賴、要共享數(shù)據(jù)那就得換更精細(xì)的拆分方式。2.3 并行的收益不是白來的阿姆達(dá)爾定律的提醒并行不是萬能加速藥。有個(gè)很實(shí)在的規(guī)律叫阿姆達(dá)爾定律一個(gè)程序里能并行的部分越多加速比才越高如果有一大段必須串行執(zhí)行那不管你加多少核總時(shí)間也降不下去。打個(gè)比方十個(gè)人搬磚很快但如果只有一把鑰匙、必須排隊(duì)開門那開門這段時(shí)間誰也幫不上忙。這提醒我們做并行優(yōu)化先找“串行瓶頸”在哪。很多時(shí)候程序慢不是核不夠而是某一段邏輯天生只能一個(gè)人干。盲目加線程、加進(jìn)程可能只是讓更多工人在門口排隊(duì)反而增加調(diào)度開銷。理解這一點(diǎn)你就不會(huì)把并行當(dāng)成“無腦加速開關(guān)”而是帶著判斷去用它。3. 并行計(jì)算和它的“親戚們”別把并發(fā)、分布式、向量化混成一鍋3.1 并行與并發(fā)一個(gè)在“同時(shí)干”一個(gè)在“輪流干”這是初學(xué)者最容易混的一對(duì)詞。并行強(qiáng)調(diào)的是同一時(shí)刻真的有多個(gè)任務(wù)在同時(shí)推進(jìn)比如四核同時(shí)算四份數(shù)據(jù)。并發(fā)強(qiáng)調(diào)的是“看起來同時(shí)在處理多件事”但底層可能只有一個(gè)核靠快速切換輪流干就像一個(gè)人同時(shí)接三個(gè)電話其實(shí)是來回切換。為什么容易混因?yàn)槿粘?谡Z里這倆詞幾乎不分。怎么區(qū)分看硬件并行需要多個(gè)物理計(jì)算單元真正同時(shí)工作并發(fā)是一種任務(wù)組織方式單核也能并發(fā)。寫程序時(shí)并發(fā)關(guān)心的是“怎么把任務(wù)調(diào)度得井井有條”并行關(guān)心的是“怎么讓多個(gè)單元同時(shí)出力”。兩者常常一起用但概念不能劃等號(hào)。3.2 并行與分布式一個(gè)在“一間屋”一個(gè)在“幾棟樓”并行計(jì)算通常指同一臺(tái)機(jī)器內(nèi)的多個(gè)核心協(xié)同共享內(nèi)存、通信極快。分布式計(jì)算則是把任務(wù)分散到多臺(tái)機(jī)器上機(jī)器之間靠網(wǎng)絡(luò)通信延遲高、還可能斷線。打個(gè)比方并行是同一個(gè)車間里的工人協(xié)作喊一嗓子就聽見分布式是幾個(gè)城市的分廠協(xié)同得靠電話和物流。為什么混因?yàn)閮烧吣繕?biāo)都是“多算力一起干活”。區(qū)分的關(guān)鍵在通信成本和故障模型并行里內(nèi)存訪問是納秒級(jí)分布式里網(wǎng)絡(luò)往返是毫秒級(jí)甚至更久而且分布式還得考慮某臺(tái)機(jī)器掛了怎么辦。所以同一套拆分思路落到分布式上要額外處理數(shù)據(jù)一致性和容錯(cuò)不能照搬。3.3 并行與向量化一個(gè)靠“人多”一個(gè)靠“手快”向量化是指讓一條指令同時(shí)處理多個(gè)數(shù)據(jù)比如 SIMD 技術(shù)一個(gè)操作搞定一組數(shù)。它不增加“工人數(shù)量”而是讓每個(gè)工人一次搬多塊磚。并行則是增加工人。兩者可以疊加多個(gè)核心各自再做向量化性能還能再上一層。容易混是因?yàn)槎冀小凹铀佟?。區(qū)分看粒度向量化是指令級(jí)的細(xì)粒度并行通常由編譯器或特定寫法觸發(fā)并行是任務(wù)級(jí)或線程級(jí)的粗粒度拆分需要你主動(dòng)設(shè)計(jì)任務(wù)怎么分。搞不清這點(diǎn)你可能以為寫了循環(huán)就自動(dòng)并行了其實(shí)編譯器只是幫你做了向量化核還是只用了一個(gè)。3.4 常見誤區(qū)核越多越快、線程越多越好最后一個(gè)高頻誤區(qū)以為加核、加線程就一定更快。實(shí)際上線程太多會(huì)帶來上下文切換開銷進(jìn)程太多會(huì)爭(zhēng)搶內(nèi)存帶寬任務(wù)拆得太碎則調(diào)度成本超過計(jì)算本身。就像搬磚叫來一百個(gè)人樓梯口擠成一團(tuán)反而誰都動(dòng)不了。怎么避開先測(cè)量再優(yōu)化用工具看程序到底卡在哪是計(jì)算密集還是等待密集串行部分占多少。然后小步試線程數(shù)從核心數(shù)起步觀察耗時(shí)變化找到拐點(diǎn)。記住并行是手段不是目的目標(biāo)是總時(shí)間更短而不是數(shù)字上“看起來更熱鬧”。4. 什么時(shí)候該上并行什么時(shí)候別硬上一份判斷清單4.1 適合并行的場(chǎng)景長(zhǎng)什么樣判斷一個(gè)任務(wù)該不該并行先看它能不能被“切開”。如果任務(wù)能自然拆成許多互不依賴的小塊比如對(duì)一大批圖片做同樣的濾鏡處理、對(duì)海量數(shù)據(jù)做獨(dú)立統(tǒng)計(jì)、矩陣乘法里各元素的計(jì)算那并行就是天作之合。這類任務(wù)有個(gè)共同點(diǎn)每塊的計(jì)算量足夠大大到調(diào)度開銷可以忽略。再看數(shù)據(jù)規(guī)模。數(shù)據(jù)量小的時(shí)候串行幾毫秒就跑完了你花力氣拆任務(wù)、開進(jìn)程光啟動(dòng)開銷就比計(jì)算還長(zhǎng)純屬折騰。數(shù)據(jù)量大、單次計(jì)算重并行的收益才明顯。還有一個(gè)信號(hào)程序跑起來時(shí)你打開任務(wù)管理器發(fā)現(xiàn)只有一個(gè)核在滿負(fù)荷其余在摸魚——這就是典型的“該并行卻沒并行”。4.2 不適合硬上的情況有些任務(wù)天生串行。比如每一步都依賴上一步結(jié)果的計(jì)算像遞推、某些圖算法你沒法讓第二步和第一步同時(shí)跑。還有些任務(wù)共享狀態(tài)極多加鎖、同步的成本高到把并行收益吃光這時(shí)候硬上并行程序不但不快還可能因?yàn)樗梨i、競(jìng)態(tài)條件翻車調(diào)試起來全是血淚經(jīng)驗(yàn)。另外I/O 密集型的任務(wù)要分清如果瓶頸在讀寫磁盤或網(wǎng)絡(luò)等待那多開線程可能有用讓等待重疊但這更接近并發(fā)調(diào)度而非純計(jì)算并行。別把“等得久”誤當(dāng)成“算得慢”方向錯(cuò)了加多少核都白搭。判斷維度適合并行不適合硬上任務(wù)依賴子任務(wù)相互獨(dú)立強(qiáng)前后依賴、必須串行數(shù)據(jù)規(guī)模數(shù)據(jù)量大、單次計(jì)算重?cái)?shù)據(jù)極小、秒級(jí)完成共享狀態(tài)少量或可分區(qū)大量共享、頻繁加鎖瓶頸類型計(jì)算密集I/O 等待為主或串行段過長(zhǎng)4.3 一個(gè)具體例子圖像批處理的取舍假設(shè)某圖像處理 Demo 要對(duì)上萬張圖片做同一種降噪處理。每張圖互不相關(guān)單張?zhí)幚砗臅r(shí)幾十毫秒這就是典型的可并行任務(wù)。做法是把圖片列表切成若干份開多個(gè)進(jìn)程各處理一份進(jìn)程數(shù)設(shè)成核心數(shù)附近觀察耗時(shí)找最優(yōu)值。這里的關(guān)鍵不是代碼多復(fù)雜而是“切得合理”切太細(xì)進(jìn)程調(diào)度開銷上來切太粗負(fù)載不均有的核早早干完在等。反過來如果這個(gè) Demo 里還有一步是“把所有圖片按處理結(jié)果排序后再統(tǒng)一輸出”那排序和匯總這段就是串行瓶頸并行只能加速前面的處理段。這時(shí)候更聰明的做法是先把能并行的部分榨干再評(píng)估串行段是否值得優(yōu)化而不是一味堆進(jìn)程。從那以后我每次動(dòng)手做并行之前都會(huì)先問自己三個(gè)問題任務(wù)能不能切、切完收益夠不夠大、串行瓶頸在哪。想清楚再寫比寫完發(fā)現(xiàn)只快了一點(diǎn)點(diǎn)要省心得多。希望這份梳理能幫你在遇到“并行計(jì)算”這個(gè)詞時(shí)不再一頭霧水而是能一眼看出它適不適合眼前這攤活兒。本文還有配套的精品資源點(diǎn)擊獲取