用GPU通用并行計算繪制曼德勃羅特集圖形 下篇
上篇中我們用DirectX Compute Shader在顯卡上編寫了一個并行算法來計算好看的曼德勃羅特集迭代數(shù)圖形。那么使用顯卡進行通用計算到底有多少優(yōu)勢呢?我們本次就來比較一番。首先我們在CPU上也實現(xiàn)該算法。為了方便起見我們設計了一個類:
class CPUCalc { private: int m_stride; int m_width; int m_height; float m_realMin; float m_imagMin; float m_scaleReal; float m_scaleImag; unsigned char* m_pData; void CalculatePoint(unsigned int x, unsigned int y); public: CPUCalc(int stride, int width, int height, float rmin, float rmax, float imin, float imax, unsigned char* pData) : m_stride(stride), m_width(width), m_height(height), m_realMin(rmin), m_imagMin(imin), m_scaleReal(0), m_scaleImag(0), m_pData(pData) { m_scaleReal = (rmax - rmin) / width; m_scaleImag = (imax - imin) / height; } void Calculate(); }; |
在HLSL代碼中放在Constant Buffer中的數(shù)據(jù),現(xiàn)在放在了類的成員處。注意我們這個類可以計算自定義復平面區(qū)間的曼德勃羅特集。rmin和rmax表示復平面的實數(shù)軸范圍而imin、imax則表示復平面的虛數(shù)軸范圍。這些參數(shù)的意義和上次HLSL中用的參數(shù)是一樣的,如果想自己實現(xiàn)該程序可以參考一下。
下面則是類的實現(xiàn)。我們用幾乎和HLSL一模一樣的做法來實現(xiàn)。其中某些HLSL的內(nèi)置方法采用類似的C++實現(xiàn)代替:
#include <algorithm> #include <math.h> #include "CPUCalc.h" using std::max; typedef unsigned int uint; const uint MAX_ITER = 4096; struct float2 { float x; float y; }; inline float smoothstep(const float minv, const float maxv, const float v) { if (v < minv) return 0.0f; else if (v > maxv) return 1.0f; else return (v - minv) / (maxv - minv); } inline uint ComposeColor(uint index) { if (index == MAX_ITER) return 0xff000000; uint red, green, blue; float phase = index * 3.0f / MAX_ITER; red = (uint)(max(0.0f, phase - 2.0f) * 255.0f); green = (uint)(smoothstep(0.0f, 1.0f, phase - 1.3f) * 255.0f); blue = (uint)(max(0.0f, 1.0f - abs(phase - 1.0f)) * 255.0f); return 0xff000000 | (red << 16) | (green << 8) | blue; } void CPUCalc::CalculatePoint(uint x, uint y) { float2 c; c.x = m_realMin + (x * m_scaleReal); c.y = m_imagMin + ((m_width - y) * m_scaleImag); float2 z; z.x = 0.0f; z.y = 0.0f; float temp, lengthSqr; uint count = 0; do { temp = z.x * z.x - z.y * z.y + c.x; z.y = 2 * z.x * z.y + c.y; z.x = temp; lengthSqr = z.x * z.x + z.y * z.y; count++; } while ((lengthSqr < 4.0f) && (count < MAX_ITER)); //write to result uint currentIndex = x * 4 + y * m_stride; uint& pPoint = *reinterpret_cast<uint*>(m_pData + currentIndex); pPoint = ComposeColor(static_cast<uint>(log((float)count) / log((float)MAX_ITER) * MAX_ITER)); } void CPUCalc::Calculate() { #pragma omp parallel for for (int y = 0; y < m_height; y++) for (int x = 0; x < m_width; x++) { CalculatePoint(x, y); } } |
最后我們增加了一個驅(qū)動運算的程序:Calculate()成員函數(shù)。它的實現(xiàn)中采用了OpenMP指令(#pragma omp)。OpenMP是C++的一個擴展,用于實現(xiàn)統(tǒng)一地址空間的并行算法。這里采用的是一種靜態(tài)任務分配的做法,將for轉(zhuǎn)化為多個線程并行執(zhí)行。這個方法對曼德勃羅特集來說有一個弊端,一會我們再詳細討論。
影響這個程序性能的主要有三點:1、輸出像素的尺寸(參數(shù)中的width和height控制);2、最大迭代次數(shù)(常數(shù)MAX_ITER控制);3、所選的復平面區(qū)域(參數(shù)中的rmin、rmax、imin、imax控制)。因為復平面中各個點的迭代次數(shù)都不一樣,所以無法確定算法的復雜度。按不超過最大迭代數(shù)來記,是一個系數(shù)很大的O(N)算法。我們這次測試固定所選的復平面區(qū)間為實數(shù)軸[-1.101,-1.099]以及虛數(shù)軸[2.229i,2.231i]的范圍。它的圖形是上篇中最后演示迭代次數(shù)那一組圖。該區(qū)間有相當大的運算量。然后我們分別固定最大迭代數(shù)和輸出像素數(shù),并變動另外一個參數(shù)進行多次測量,比較CPU和GPU進行運算的性能。
這次測試采用的CPU是Intel Core i7 920,具有四個核心,默認主頻2.66GHz,搭配6GB DDR3-1333內(nèi)存。它還具有超線程技術(shù),可以同時運行8個線程。GPU是AMD Ati HD5850顯卡,默認頻率725MHz(本次超頻775MHz)搭配1GB 1250MHz GDDR5顯存。該顯卡新片具有18組SIMD處理器,共有1440個Stream Core運算單元。
首先我們固定最大迭代次數(shù)為512次,然后依次輸出像素512x512、1024x1024、2048x2048、4096x4096、8192x8192、16384x16384像素的圖片。下面是結(jié)果(時間單位為毫秒):
輸出像素 | CPU成績 | GPU成績 | 速度比(G:C) |
512 x 512 | 213 | 23 | 9.26 |
1024 x 1024 | 635 | 83 | 7.65 |
2048 x 2048 | 2403 | 312 | 7.70 |
4096 x 4096 | 9279 | 1227 | 7.56 |
8192 x 8192 | 37287 | 4894 | 7.61 |
16384 x 16384 | 152015 | 35793 | 4.24 |
前五項數(shù)據(jù)的圖表:
我們可以看到,GPU具有非常巨大的性能優(yōu)勢。即使是8個線程同時運轉(zhuǎn)的酷睿i7處理器也完全不能同GPU相比。還可以觀察到一些事實:在像素增大的時候GPU和CPU以類似的速度變慢。GPU:CPU的速度比在很大范圍內(nèi)都保持在7.6倍左右。而當像素數(shù)增大到16384x16384之后GPU的性能突然下降了,速度比降低到了4.24。這是因為在如此像素數(shù)下,顯卡的顯存已經(jīng)不夠用了,CPU分配了內(nèi)存給顯卡做虛擬顯存使用。這個過程消耗了不少時間,導致顯卡的性能大大受損。在處理較大數(shù)據(jù)的時候顯存容量顯得非常重要。因此nVidia和AMD都推出了通用計算專用版顯卡(Tesla和FireStream),這些專用計算卡的一大特點就是具有更大的顯存。
接下來我們將輸出像素數(shù)固定在4096x4096上,然后測試不同的最大迭代數(shù)。從512一直增大到16384。下面是測試結(jié)果(時間單位為毫秒):
最大迭代數(shù) | CPU成績 | GPU成績 | 速度比(G:C) |
512 | 9363 | 1247 | 7.51 |
1024 | 18042 | 1513 | 11.92 |
2048 | 35132 | 2058 | 17.07 |
4096 | 68398 | 2897 | 23.61 |
8192 | 135074 | 4347 | 31.07 |
16384 | 266547 | 7082 | 37.63 |
前五項數(shù)據(jù)的圖表:
這次我們看到GPU出奇的優(yōu)勢。在迭代數(shù)增加到16384時,GPU竟然比CPU快出37倍之多!為什么會這樣呢?除了GPU本身并行計算確實強勁之外,我們的CPU算法也有一個問題。在曼德勃羅特集的運算當中,不是每個點都能達到最大迭代數(shù)。相當多的點在不到最大迭代數(shù)之前就已經(jīng)計算完了。如果我們將復平面的點均勻分給多個線程的話,那就會有一些線程先計算完成,有一些線程后計算完成的問題。如果我們觀察運算過程中的CPU占用率就會發(fā)現(xiàn),差不多一半的時間里CPU都不能夠100%地充分利用:
當?shù)鷶?shù)增大的時候,這種現(xiàn)象就更加明顯了。而GPU中我們的任務非常細(每個線程僅負責一個坐標點的計算),DirectX實現(xiàn)了動態(tài)線程分配,使得GPU中一旦有空閑的運算單元就可以分配新的線程進行運算。因此我們CPU程序吃了很大的虧。不過,若我們重新編寫CPU算法,也采用動態(tài)分配的話,就可以顯著提高CPU利用率,縮小這一差距。這個任務就留給讀者來完成了,試試看你能否寫出讓CPU盡可能保持100%的曼德勃羅特集算法。
原文:http://www.cnblogs.com/Ninputer/archive/2009/11/25/1610079.html