遇见C++ AMP:在GPU上做并行计算
遇見C++ AMP:在GPU上做并行計(jì)算
?
Written by Allen Lee
?
I see all the young believers, your target audience. I see all the old deceivers; we all just sing their song.
– Marilyn Manson, Target Audience (Narcissus Narcosis)
?
從CPU到GPU
? ? ? 在《遇見C++ PPL:C++的并行和異步》里,我們介紹了如何使用C++ PPL在CPU上做并行計(jì)算,這次,我們會(huì)把舞臺(tái)換成GPU,介紹如何使用C++ AMP在上面做并行計(jì)算。
? ? ? 為什么選擇在GPU上做并行計(jì)算呢?現(xiàn)在的多核CPU一般都是雙核或四核的,如果把超線程技術(shù)考慮進(jìn)來,可以把它們看作四個(gè)或八個(gè)邏輯核,但現(xiàn)在的GPU動(dòng)則就上百個(gè)核,比如中端的NVIDIA GTX 560 SE就有288個(gè)核,頂級(jí)的NVIDIA GTX 690更有多達(dá)3072個(gè)核,這些超多核(many-core)GPU非常適合大規(guī)模并行計(jì)算。
? ? ? 接下來,我們將會(huì)在《遇見C++ PPL:C++的并行和異步》的基礎(chǔ)上,對(duì)并行計(jì)算正弦值的代碼進(jìn)行一番改造,使之可以在GPU上運(yùn)行。如果你沒讀過那篇文章,我建議你先去讀一讀它的第一節(jié)。此外,本文也假設(shè)你對(duì)C++ Lambda有所了解,否則,我建議你先去讀一讀《遇見C++ Lambda》。
?
并行計(jì)算正弦值
? ? ? 首先,包含/引用相關(guān)的頭文件/命名空間,如代碼1所示。amp.h是C++ AMP的頭文件,包含了相關(guān)的函數(shù)和類,它們位于concurrency命名空間之內(nèi)。amp_math.h包含了常用的數(shù)學(xué)函數(shù),如sin函數(shù),concurrency::fast_math命名空間里的函數(shù)只支持單精度浮點(diǎn)數(shù),而concurrency::precise_math命名空間里的函數(shù)則對(duì)單精度浮點(diǎn)數(shù)和雙精度浮點(diǎn)數(shù)均提供支持。
代碼 1
? ? ? 把浮點(diǎn)數(shù)的類型從double改成float,如代碼2所示,這樣做是因?yàn)椴⒎撬蠫PU都支持雙精度浮點(diǎn)數(shù)的運(yùn)算。另外,std和concurrency兩個(gè)命名空間都有一個(gè)array類,為了消除歧義,我們需要在array前面加上"std::"前綴,以便告知編譯器我們使用的是STL的array類。
代碼 2
? ? ? 接著,創(chuàng)建一個(gè)array_view對(duì)象,把前面創(chuàng)建的array對(duì)象包裝起來,如代碼3所示。array_view對(duì)象只是一個(gè)包裝器,本身不能包含任何數(shù)據(jù),必須和真正的容器搭配使用,如C風(fēng)格的數(shù)組、STL的array對(duì)象或vector對(duì)象。當(dāng)我們創(chuàng)建array_view對(duì)象時(shí),需要通過類型參數(shù)指定array_view對(duì)象里的元素的類型以及它的維度,并通過構(gòu)造函數(shù)的參數(shù)指定對(duì)應(yīng)維度的長(zhǎng)度以及包含實(shí)際數(shù)據(jù)的容器。
代碼 3
? ? ? 代碼3創(chuàng)建了一個(gè)一維的array_view對(duì)象,這個(gè)維度的長(zhǎng)度和前面的array對(duì)象的長(zhǎng)度一樣,這個(gè)包裝看起來有點(diǎn)多余,為什么要這樣做?這是因?yàn)樵贕PU上運(yùn)行的代碼無法直接訪問系統(tǒng)內(nèi)存里的數(shù)據(jù),需要array_view對(duì)象出來充當(dāng)一個(gè)橋梁的角色,使得在GPU上運(yùn)行的代碼可以通過它間接訪問系統(tǒng)內(nèi)存里的數(shù)據(jù)。事實(shí)上,在GPU上運(yùn)行的代碼訪問的并非系統(tǒng)內(nèi)存里的數(shù)據(jù),而是復(fù)制到顯存的副本,而負(fù)責(zé)把這些數(shù)據(jù)從系統(tǒng)內(nèi)存復(fù)制到顯存的正是array_view對(duì)象,這個(gè)過程是自動(dòng)的,無需我們干預(yù)。
? ? ? 有了前面這些準(zhǔn)備,我們就可以著手編寫在GPU上運(yùn)行的代碼了,如代碼4所示。parallel_for_each函數(shù)可以看作C++ AMP的入口點(diǎn),我們通過extent對(duì)象告訴它創(chuàng)建多少個(gè)GPU線程,通過Lambda告訴它這些GPU線程運(yùn)行什么代碼,我們通常把這個(gè)代碼稱作Kernel。
代碼 4
? ? ? 我們希望每個(gè)GPU線程可以完成和結(jié)果集里的某個(gè)元素對(duì)應(yīng)的一組操作,比如說,我們需要計(jì)算10個(gè)浮點(diǎn)數(shù)的正弦值,那么,我們希望創(chuàng)建10個(gè)GPU線程,每個(gè)線程依次完成讀取浮點(diǎn)數(shù)、計(jì)算正弦值和保存正弦值三個(gè)操作。但是,每個(gè)GPU線程運(yùn)行的代碼都是一樣的,如何區(qū)分不同的GPU線程,并定位需要處理的數(shù)據(jù)呢?
? ? ? 這個(gè)時(shí)候就輪到index對(duì)象出場(chǎng)了,我們的array_view對(duì)象是一維的,因此index對(duì)象的類型是index<1>,這個(gè)維度的長(zhǎng)度是10,因此將會(huì)產(chǎn)生從0到9的10個(gè)index對(duì)象,每個(gè)GPU線程對(duì)應(yīng)其中一個(gè)index對(duì)象。這個(gè)index對(duì)象將會(huì)通過Lambda的參數(shù)傳給我們,而我們將會(huì)在Kernel里通過這個(gè)index對(duì)象找到當(dāng)前GPU線程需要處理的數(shù)據(jù)。
? ? ? 既然Lambda的參數(shù)只傳遞index對(duì)象,那Kernel又是如何與外界交換數(shù)據(jù)的呢?我們可以通過閉包捕獲當(dāng)前上下文的變量,這使我們可以靈活地操作多個(gè)數(shù)據(jù)源和結(jié)果集,因此沒有必要提供返回值。從這個(gè)角度來看,C++ AMP的parallel_for_each函數(shù)在用法上類似于C++ PPL的parallel_for函數(shù),如代碼5所示,我們傳給前者的extent對(duì)象代替了我們傳給后者的起止索引值。
代碼 5
? ? ? 那么,Kernel右邊的restrict(amp)修飾符又是怎么一回事呢?Kernel最終是在GPU上運(yùn)行的,不管以什么樣的形式,restrict(amp)修飾符正是用來告訴編譯器這點(diǎn)的。當(dāng)編譯器看到restrict(amp)修飾符時(shí),它會(huì)檢查Kernel是否使用了不支持的語(yǔ)言特性,如果有,編譯過程中止,并列出錯(cuò)誤,否則,Kernel會(huì)被編譯成HLSL,并交給DirectCompute運(yùn)行。Kernel可以調(diào)用其他函數(shù),但這些函數(shù)必須添加restrict(amp)修飾符,比如代碼4的sin函數(shù)。
? ? ? 計(jì)算完畢之后,我們可以通過一個(gè)for循環(huán)輸出array_view對(duì)象的數(shù)據(jù),如代碼6所示。當(dāng)我們?cè)贑PU上首次通過索引器訪問array_view對(duì)象時(shí),它會(huì)把數(shù)據(jù)從顯存復(fù)制回系統(tǒng)內(nèi)存,這個(gè)過程是自動(dòng)的,無需我們干預(yù)。
代碼 6
? ? ? 哇,不知不覺已經(jīng)講了這么多,其實(shí),使用C++ AMP一般只涉及到以下三步:
其他的事情,如顯存的分配和釋放、GPU線程的規(guī)劃和管理,C++ AMP會(huì)幫我們處理的。
?
并行計(jì)算矩陣之和
? ? ? 上一節(jié)我們通過一個(gè)簡(jiǎn)單的示例了解C++ AMP的使用步驟,接下來我們將會(huì)通過另一個(gè)示例深入了解array_view、extent和index在二維場(chǎng)景里的用法。
? ? ? 假設(shè)我們現(xiàn)在要計(jì)算兩個(gè)100 x 100的矩陣之和,首先定義矩陣的行和列,然后通過create_matrix函數(shù)創(chuàng)建兩個(gè)vector對(duì)象,接著創(chuàng)建一個(gè)vector對(duì)象用于存放矩陣之和,如代碼7所示。
代碼 7
? ? ? create_matrix函數(shù)的實(shí)現(xiàn)很簡(jiǎn)單,它接受矩陣的總?cè)萘?#xff08;行和列之積)作為參數(shù),然后創(chuàng)建并返回一個(gè)包含100以內(nèi)的隨機(jī)數(shù)的vector對(duì)象,如代碼8所示。
代碼 8
? ? ? 值得提醒的是,當(dāng)create_matrix函數(shù)執(zhí)行"return matrix;"時(shí),會(huì)把vector對(duì)象拷貝到一個(gè)臨時(shí)對(duì)象,并把這個(gè)臨時(shí)對(duì)象返回給調(diào)用方,而原來的vector對(duì)象則會(huì)因?yàn)槌鲎饔糜蚨詣?dòng)銷毀,但我們可以通過編譯器的Named Return Value Optimization對(duì)此進(jìn)行優(yōu)化,因此不必?fù)?dān)心按值返回會(huì)帶來性能問題。
? ? ? 雖然我們通過行和列等二維概念定義矩陣,但它的實(shí)現(xiàn)是通過vector對(duì)象模擬的,因此在使用的時(shí)候我們需要做一下索引變換,矩陣的第m行第n列元素對(duì)應(yīng)的vector對(duì)象的索引是m * columns + n(m、n均從0開始計(jì)算)。假設(shè)我們要用vector對(duì)象模擬一個(gè)3 x 3的矩陣,如圖1所示,那么,要訪問矩陣的第2行第0列元素,應(yīng)該使用索引6(2 * 3 + 0)訪問vector對(duì)象。
圖 1
? ? ? 接下來,我們需要?jiǎng)?chuàng)建三個(gè)array_view對(duì)象,分別包裝前面創(chuàng)建的三個(gè)vector對(duì)象,創(chuàng)建的時(shí)候先指定行的大小,再指定列的大小,如代碼9所示。
代碼 9
? ? ? 因?yàn)槲覀儎?chuàng)建的是二維的array_view對(duì)象,所以我們可以直接使用二維索引訪問矩陣的元素,而不必像前面那樣計(jì)算對(duì)應(yīng)的索引。還是以3 x 3的矩陣為例,如圖2所示,vector對(duì)象會(huì)被分成三段,每段包含三個(gè)元素,第一段對(duì)應(yīng)array_view對(duì)象的第一行,第二段對(duì)應(yīng)第二行,如此類推。如果我們想訪問矩陣的第2行第0列的元素,可以直接使用索引 (2, 0) 訪問array_view對(duì)象,這個(gè)索引對(duì)應(yīng)vector對(duì)象的索引6。
圖 2
? ? ? 考慮到第一、二個(gè)array_view對(duì)象的數(shù)據(jù)流動(dòng)方向是從系統(tǒng)內(nèi)存到顯存,我們可以把它們的第一個(gè)類型參數(shù)改為const int,如代碼10所示,表示它們?cè)贙ernel里是只讀的,不會(huì)對(duì)它包裝的vector對(duì)象產(chǎn)生任何影響。至于第三個(gè)array_view對(duì)象,由于它只是用來輸出計(jì)算結(jié)果,我們可以在調(diào)用parallel_for_each函數(shù)之前調(diào)用array_view對(duì)象的discard_data成員函數(shù),表明我們對(duì)它包裝的vector對(duì)象的數(shù)據(jù)不感興趣,不必把它們從系統(tǒng)內(nèi)存復(fù)制到顯存。
代碼 10
? ? ? 有了這些準(zhǔn)備,我們就可以著手編寫Kernel了,如代碼11所示。我們把第三個(gè)array_view對(duì)象的extent傳給parallel_for_each函數(shù),由于這個(gè)矩陣是100 x 100的,parallel_for_each函數(shù)會(huì)創(chuàng)建10,000個(gè)GPU線程,每個(gè)GPU線程計(jì)算這個(gè)矩陣的一個(gè)元素。由于我們?cè)L問的array_view對(duì)象是二維的,索引的類型也要改為相應(yīng)的index<2>。
代碼 11
? ? ? 看到這里,你可能會(huì)問,GPU真能創(chuàng)建這么多個(gè)線程嗎?這取決于具體的GPU,比如說,NVIDIA GTX 690有16個(gè)多處理器(Kepler架構(gòu),每個(gè)多處理器有192個(gè)CUDA核),每個(gè)多處理器的最大線程數(shù)是2048,因此可以同時(shí)容納最多32,768個(gè)線程;而NVIDIA GTX 560 SE擁有9個(gè)多處理器(Fermi架構(gòu),每個(gè)多處理器有32個(gè)CUDA核),每個(gè)多處理器的最大線程數(shù)是1536,因此可以同時(shí)容納最多13,824個(gè)線程。
? ? ? 計(jì)算完畢之后,我們可以在CPU上通過索引器訪問計(jì)算結(jié)果,代碼12向控制臺(tái)輸出結(jié)果矩陣的第14行12列元素。
代碼 12
?
async + continuation
? ? ? 掌握了C++ AMP的基本用法之后,我們很自然就想知道parallel_for_each函數(shù)會(huì)否阻塞當(dāng)前CPU線程。parallel_for_each函數(shù)本身是同步的,它負(fù)責(zé)發(fā)起Kernel的運(yùn)行,但不會(huì)等到Kernel的運(yùn)行結(jié)束才返回。以代碼13為例,當(dāng)parallel_for_each函數(shù)返回時(shí),即使Kernel的運(yùn)行還沒結(jié)束,checkpoint 1位置的代碼也會(huì)照常運(yùn)行,從這個(gè)角度來看,parallel_for_each函數(shù)是異步的。但是,當(dāng)我們通過array_view對(duì)象訪問計(jì)算結(jié)果時(shí),如果Kernel的運(yùn)行還沒結(jié)束,checkpoint 2位置的代碼會(huì)卡住,直到Kernel的運(yùn)行結(jié)束,array_view對(duì)象把數(shù)據(jù)從顯存復(fù)制到系統(tǒng)內(nèi)存為止。
代碼 13
? ? ? 既然Kernel的運(yùn)行是異步的,我們很自然就會(huì)希望C++ AMP能夠提供類似C++ PPL的continuation。幸運(yùn)的是,array_view對(duì)象提供一個(gè)synchronize_async成員函數(shù),它返回一個(gè)concurrency::completion_future對(duì)象,我們可以通過這個(gè)對(duì)象的then成員函數(shù)實(shí)現(xiàn)continuation,如代碼14所示。事實(shí)上,這個(gè)then成員函數(shù)就是通過C++ PPL的task對(duì)象實(shí)現(xiàn)的。
代碼 14
?
你可能會(huì)問的問題
? ? ? 1. 開發(fā)C++ AMP程序需要什么條件?
? ? ? 你需要Visual Studio 2012以及一塊支持DirectX 11的顯卡,Visual C++ 2012 Express應(yīng)該也可以,如果你想做GPU調(diào)試,你還需要Windows 8操作系統(tǒng)。運(yùn)行C++ AMP程序需要Windows 7/Windows 8以及一塊支持DirectX 11的顯卡,部署的時(shí)候需要把C++ AMP的運(yùn)行時(shí)(vcamp110.dll)放在程序可以找到的目錄里,或者在目標(biāo)機(jī)器上安裝Visual C++ 2012 Redistributable Package。
? ? ? 2. C++ AMP是否支持其他語(yǔ)言?
? ? ? C++ AMP只能在C++里使用,其他語(yǔ)言可以通過相關(guān)機(jī)制間接調(diào)用你的C++ AMP代碼:
- How to use C++ AMP from C#
- How to use C++ AMP from C# using WinRT
- How to use C++ AMP from C++ CLR app
- Using C++ AMP code in a C++ CLR project
? ? ? 3. C++ AMP是否支持其他平臺(tái)?
? ? ? 目前C++ AMP只支持Windows平臺(tái),不過,微軟發(fā)布了C++ AMP開放標(biāo)準(zhǔn),支持任何人在任何平臺(tái)上實(shí)現(xiàn)它。如果你希望在其他平臺(tái)上利用GPU做并行計(jì)算,你可以考慮其他技術(shù),比如NVIDIA的CUDA(只支持NVIDIA的顯卡),或者OpenCL,它們都支持多個(gè)平臺(tái)。
? ? ? 4. 能否推薦一些C++ AMP的學(xué)習(xí)資料?
? ? ? 目前還沒有C++ AMP的書,Kate Gregory和Ade Miller正在寫一本關(guān)于C++ AMP的書,希望很快能夠看到它。下面推薦一些在線學(xué)習(xí)資料:
- C++ AMP open specification
- Parallel Programming in Native Code (team blog)
- C++ AMP (C++ Accelerated Massive Parallelism)
- C++ AMP Videos
?
*聲明:本文已經(jīng)首發(fā)于InfoQ中文站,版權(quán)所有,《遇見C++ AMP:在GPU上做并行計(jì)算》,如需轉(zhuǎn)載,請(qǐng)務(wù)必附帶本聲明,謝謝。
總結(jié)
以上是生活随笔為你收集整理的遇见C++ AMP:在GPU上做并行计算的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: poj 1384 完全背包
- 下一篇: spring mvc-使用Servlet