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