登 录
註 冊
论坛
微波仿真网
注册
登录论坛可查看更多信息
微波仿真论坛
>
程序
>
深入浅出谈CUDA(基于显卡的高性能并行计算)
发帖
回复
1
2
3
4
5
6
...17
下一页
到第
页
确认
19174
阅读
166
回复
[
转载
]
深入浅出谈CUDA(基于显卡的高性能并行计算)
离线
cem-uestc
UID :9061
注册:
2008-03-07
登录:
2019-01-05
发帖:
2575
等级:
荣誉管理员
0楼
发表于: 2008-11-30 20:07:04
[post]现在基于CUDA的显卡进行科学计算比较热门,现转转一篇很好的关于这方面的资料。
xU F5
----------------------------------------
GUps\:ss
z7s}-w,
深入浅出谈CUDA
U?xa^QVhj
发表时间:2008-11-21
MMy\u) 4
“CUDA 是 NVIDIA 的 GPGPU 模型,它使用 C 语言为基础,可以直接以大多数人熟悉的 C 语言,写出在显示芯片上执行的程序,而不需要去学习特定的显示芯片的指令或是特殊的结构。”
!y&<IT(\4
“CUDA 是 NVIDIA 的 GPGPU 模型,它使用 C 语言为基础,可以直接以大多数人熟悉的 C 语言,写出在显示芯片上执行的程序,而不需要去学习特定的显示芯片的指令或是特殊的结构。”
-PCFOm"
CUDA是什么?能吃吗?
j" 5 +"j
编者注:NVIDIA的GeFoce 8800GTX发布后,它的通用计算架构CUDA经过一年多的推广后,现在已经在有相当多的论文发表,在商业应用软件等方面也初步出现了视频编解码、金融、地质勘探、科学计算等领域的产品,是时候让我们对其作更深一步的了解。为了让大家更容易了解CUDA,我们征得Hotball的本人同意,发表他最近亲自撰写的本文。这篇文章的特点是深入浅出,也包含了hotball本人编写一些简单CUDA程序的亲身体验,对于希望了解CUDA的读者来说是非常不错的入门文章,PCINLIFE对本文的发表没有作任何的删减,主要是把一些台湾的词汇转换成大陆的词汇以及作了若干"编者注"的注释。
no,b_0@N
现代的显示芯片已经具有高度的可程序化能力,由于显示芯片通常具有相当高的内存带宽,以及大量的执行单元,因此开始有利用显示芯片来帮助进行一些计算工作的想法,即 GPGPU。CUDA 即是 NVIDIA 的 GPGPU 模型。
Q"s]<MtdS
NVIDIA 的新一代显示芯片,包括 GeForce 8 系列及更新的显示芯片都支持 CUDA。NVIDIA 免费提供 CUDA 的开发工具(包括 Windows 版本和 Linux 版本)、程序范例、文件等等,可以在 CUDA Zone 下载。
}vEMG-sxX
GPGPU 的优缺点
LlcH#L$
使用显示芯片来进行运算工作,和使用 CPU 相比,主要有几个好处:
sZ>0*S
1. 显示芯片通常具有更大的内存带宽。例如,NVIDIA 的 GeForce 8800GTX 具有超过 50GB/s 的内存带宽,而目前高阶 CPU 的内存带宽则在 10GB/s 左右。
XI>HC'.0
2. 显示芯片具有更大量的执行单元。例如 GeForce 8800GTX 具有 128 个 "stream processors",频率为 1.35GHz。CPU 频率通常较高,但是执行单元的数目则要少得多。
IP!`;?T=
3. 和高阶 CPU 相比,显卡的价格较为低廉。例如目前一张 GeForce 8800GT 包括 512MB 内存的价格,和一颗 2.4GHz 四核心 CPU 的价格相若。
^@ Xzh:
当然,使用显示芯片也有它的一些缺点:
aeE~[m
1. 显示芯片的运算单元数量很多,因此对于不能高度并行化的工作,所能带来的帮助就不大。
=eQ'^3a
2. 显示芯片目前通常只支持 32 bits 浮点数,且多半不能完全支持 IEEE 754 规格, 有些运算的精确度可能较低。目前许多显示芯片并没有分开的整数运算单元,因此整数运算的效率较差。
Ys>Z=Eky
3. 显示芯片通常不具有分支预测等复杂的流程控制单元,因此对于具有高度分支的程序,效率会比较差。
QKI g5I-
4. 目前 GPGPU 的程序模型仍不成熟,也还没有公认的标准。例如 NVIDIA 和 AMD/ATI 就有各自不同的程序模型。
f~?kx41dq
整体来说,显示芯片的性质类似 stream processor,适合一次进行大量相同的工作。CPU 则比较有弹性,能同时进行变化较多的工作。
?/fC"MJq?
CUDA 架构
ID~}pEQ
CUDA 是 NVIDIA 的 GPGPU 模型,它使用 C 语言为基础,可以直接以大多数人熟悉的 C 语言,写出在显示芯片上执行的程序,而不需要去学习特定的显示芯片的指令或是特殊的结构。
qK)T#sh
在 CUDA 的架构下,一个程序分为两个部份:host 端和 device 端。Host 端是指在 CPU 上执行的部份,而 device 端则是在显示芯片上执行的部份。Device 端的程序又称为 "kernel"。通常 host 端程序会将数据准备好后,复制到显卡的内存中,再由显示芯片执行 device 端程序,完成后再由 host 端程序将结果从显卡的内存中取回。
6J<R;g23R]
XC!Y {lp
由于 CPU 存取显卡内存时只能透过 PCI Express 接口,因此速度较慢(PCI Express x16 的理论带宽是双向各 4GB/s),因此不能太常进行这类动作,以免降低效率。
S@@#L
在 CUDA 架构下,显示芯片执行时的最小单位是 thread。数个 thread 可以组成一个 block。一个 block 中的 thread 能存取同一块共享的内存,而且可以快速进行同步的动作。
!8I80:e_~
每一个 block 所能包含的 thread 数目是有限的。不过,执行相同程序的 block,可以组成 grid。不同 block 中的 thread 无法存取同一个共享的内存,因此无法直接互通或进行同步。因此,不同 block 中的 thread 能合作的程度是比较低的。不过,利用这个模式,可以让程序不用担心显示芯片实际上能同时执行的 thread 数目限制。例如,一个具有很少量执行单元的显示芯片,可能会把各个 block 中的 thread 顺序执行,而非同时执行。不同的 grid 则可以执行不同的程序(即 kernel)。
J%IKdxa
Grid、block 和 thread 的关系,如下图所示:
wW, n~W
Ce:w^P+
每个 thread 都有自己的一份 register 和 local memory 的空间。同一个 block 中的每个 thread 则有共享的一份 share memory。此外,所有的 thread(包括不同 block 的 thread)都共享一份 global memory、constant memory、和 texture memory。不同的 grid 则有各自的 global memory、constant memory 和 texture memory。这些不同的内存的差别,会在之后讨论。
}BWT21'-Y
执行模式
C ~Doj
由于显示芯片大量并行计算的特性,它处理一些问题的方式,和一般 CPU 是不同的。主要的特点包括:
5i-VnG
1. 内存存取 latency 的问题:CPU 通常使用 cache 来减少存取主内存的次数,以避免内存 latency 影响到执行效率。显示芯片则多半没有 cache(或很小),而利用并行化执行的方式来隐藏内存的 latency(即,当第一个 thread 需要等待内存读取结果时,则开始执行第二个 thread,依此类推)。
.1:B\R((
2. 分支指令的问题:CPU 通常利用分支预测等方式来减少分支指令造成的 pipeline bubble。显示芯片则多半使用类似处理内存 latency 的方式。不过,通常显示芯片处理分支的效率会比较差。
.naSK`J,`
因此,最适合利用 CUDA 处理的问题,是可以大量并行化的问题,才能有效隐藏内存的 latency,并有效利用显示芯片上的大量执行单元。使用 CUDA 时,同时有上千个 thread 在执行是很正常的。因此,如果不能大量并行化的问题,使用 CUDA 就没办法达到最好的效率了。
(z:qj/|
CUDA Toolkit的安装
8'Iei78Ov
目前 NVIDIA 提供的 CUDA Toolkit(可从这里下载)支持 Windows (32 bits 及 64 bits 版本)及许多不同的 Linux 版本。
Zg3 /,:1
CUDA Toolkit 需要配合 C/C++ compiler。在 Windows 下,目前只支持 Visual Studio 7.x 及 Visual Studio 8(包括免费的 Visual Studio C++ 2005 Express)。Visual Studio 6 和 gcc 在 Windows 下是不支援的。在 Linux 下则只支援 gcc。
EvptGM
这里简单介绍一下在 Windows 下设定并使用 CUDA 的方式。
Z4dl'v)9
下载及安装
hf:\^w
在 Windows 下,CUDA Toolkit 和 CUDA SDK 都是由安装程序的形式安装的。CUDA Toolkit 包括 CUDA 的基本工具,而 CUDA SDK 则包括许多范例程序以及链接库。基本上要写 CUDA 的程序,只需要安装 CUDA Toolkit 即可。不过 CUDA SDK 仍值得安装,因为里面的许多范例程序和链接库都相当有用。
{n{-5Y
CUDA Toolkit 安装完后,预设会安装在 C:\CUDA 目录里。其中包括几个目录:
Ae* 6&R4
• bin -- 工具程序及动态链接库
M Al4g+es
• doc -- 文件
G4^6o[ x
• include -- header 檔
x~E\zw
• lib -- 链接库档案
;0o% hx
• open64 -- 基于 Open64 的 CUDA compiler
q4SEvP}fLx
• src -- 一些原始码
- WQ)rz
安装程序也会设定一些环境变量,包括:
P@0J!
• CUDA_BIN_PATH -- 工具程序的目录,默认为 C:\CUDA\bin
AEd9H +I
• CUDA_INC_PATH -- header 文件的目录,默认为 C:\CUDA\inc
m[nrr6 G"
• CUDA_LIB_PATH -- 链接库文件的目录,默认为 C:\CUDA\lib
t]hfq~Ft
在 Visual Studio 中使用 CUDA
MxqIB(5k
CUDA 的主要工具是 nvcc,它会执行所需要的程序,将 CUDA 程序代码编译成执行档 (或 object 檔) 。在 Visual Studio 下,我们透过设定 custom build tool 的方式,让 Visual Studio 会自动执行 nvcc。
Y+DVwz$
这里以 Visual Studio 2005 为例:
oml^f~pm
1. 首先,建立一个 Win32 Console 模式的 project(在 Application Settings 中记得勾选 Empty project),并新增一个档案,例如 main.cu。
0.&-1pw
2. 在 main.cu 上右键单击,并选择 Properties。点选 General,确定 Tool 的部份是选择 Custom Build Tool。
WJ/X`?k
3. 选择 Custom Build Step,在 Command Line 使用以下设定:
ZU;nXqjc
o Release 模式:"$(CUDA_BIN_PATH)\nvcc.exe" -ccbin "$(VCInstallDir)bin" -c -DWIN32 -D_CONSOLE -D_MBCS -Xcompiler /EHsc,/W3,/nologo,/Wp64,/O2,/Zi,/MT -I"$(CUDA_INC_PATH)" -o $(ConfigurationName)\$(InputName).obj $(InputFileName)
S])*LUi
o Debug 模式:"$(CUDA_BIN_PATH)\nvcc.exe" -ccbin "$(VCInstallDir)bin" -c -D_DEBUG -DWIN32 -D_CONSOLE -D_MBCS -Xcompiler /EHsc,/W3,/nologo,/Wp64,/Od,/Zi,/RTC1,/MTd -I"$(CUDA_INC_PATH)" -o $(ConfigurationName)\$(InputName).obj $(InputFileName)
m$VCCDv
4. 如果想要使用软件仿真的模式,可以新增两个额外的设定:
A$n:
o EmuRelease 模式:"$(CUDA_BIN_PATH)\nvcc.exe" -ccbin "$(VCInstallDir)bin" -deviceemu -c -DWIN32 -D_CONSOLE -D_MBCS -Xcompiler /EHsc,/W3,/nologo,/Wp64,/O2,/Zi,/MT -I"$(CUDA_INC_PATH)" -o $(ConfigurationName)\$(InputName).obj $(InputFileName)
i,mZg+;w
o EmuDebug 模式:"$(CUDA_BIN_PATH)\nvcc.exe" -ccbin "$(VCInstallDir)bin" -deviceemu -c -D_DEBUG -DWIN32 -D_CONSOLE -D_MBCS -Xcompiler /EHsc,/W3,/nologo,/Wp64,/Od,/Zi,/RTC1,/MTd -I"$(CUDA_INC_PATH)" -o $(ConfigurationName)\$(InputName).obj $(InputFileName)
)T@?.J`
5. 对所有的配置文件,在 Custom Build Step 的 Outputs 中加入 $(ConfigurationName)\$(InputName).obj。
A}[x))r
6. 选择 project,右键单击选择 Properties,再点选 Linker。对所有的配置文件修改以下设定:
;( (|0Xa
o General/Enable Incremental Linking:No
h\4enu9[RL
o General/Additional Library Directories:$(CUDA_LIB_PATH)
W)AfXy
o Input/Additional Dependencies:cudart.lib
SM4'3d&mf
这样应该就可以直接在 Visual Studio 的 IDE 中,编辑 CUDA 程序后,直接 build 以及执行程序了。
<=!FB8 .
第一个CUDA程序
F{E`MK~f_
CUDA 目前有两种不同的 API:Runtime API 和 Driver API,两种 API 各有其适用的范围。由于 runtime API 较容易使用,一开始我们会以 runetime API 为主。
yeLd,M/I
CUDA 的初始化
y?UB?2VN
首先,先建立一个档案 first_cuda.cu。如果是使用 Visual Studio 的话,则请先按照这里的设定方式设定 project。
mM'uRhO+
要使用 runtime API 的时候,需要 include cuda_runtime.h。所以,在程序的最前面,加上
P1&Irwb`
#include <stdio.h>
3F$N@K~s
#include <cuda_runtime.h>
Y o\%53w/
接下来是一个 InitCUDA 函式,会呼叫 runtime API 中,有关初始化 CUDA 的功能:
EKUiX#p:M
bool InitCUDA()
HfEl TC:3f
{
|Es,$
int count;
jd.w7.8
_FCg5F2U
cudaGetDeviceCount(&count);
|<JBoE]3B
if(count == 0) {
M63t4; 0A
fprintf(stderr, "There is no device.\n");
$ve*j=p
return false;
Ap> H-/C
}
,M Ugww!.
Q"K`~QF"
int i;
^sF(IV[>
for(i = 0; i < count; i++) {
sj&1I.@,>
cudaDeviceProp prop;
-YS9u[
if(cudaGetDeviceProperties(&prop, i) == cudaSuccess) {
Oo/@A_JO@
if(prop.major >= 1) {
L-Mf{z
break;
_e "
}
-PaR&0Tt
}
AG|:mQO
}
)BZ6QO`5n
*9US>m Vy
if(i == count) {
.ZupsS9l
fprintf(stderr, "There is no device supporting CUDA 1.x.\n");
w& )ApfL
return false;
P:UR:y([
}
'VV"$`Fu"
e 3TKg
cudaSetDevice(i);
$49;\pBZl
{a,U{YJ\H
return true;
cRWYS[O?-
}
R]l2,0:
这个函式会先呼叫 cudaGetDeviceCount 函式,取得支持 CUDA 的装置的数目。如果系统上没有支持 CUDA 的装置,则它会传回 1,而 device 0 会是一个仿真的装置,但不支持 CUDA 1.0 以上的功能。所以,要确定系统上是否有支持 CUDA 的装置,需要对每个 device 呼叫 cudaGetDeviceProperties 函式,取得装置的各项数据,并判断装置支持的 CUDA 版本(prop.major 和 prop.minor 分别代表装置支持的版本号码,例如 1.0 则 prop.major 为 1 而 prop.minor 为 0)。
U:gvK8n
透过 cudaGetDeviceProperties 函式可以取得许多数据,除了装置支持的 CUDA 版本之外,还有装置的名称、内存的大小、最大的 thread 数目、执行单元的频率等等。详情可参考 NVIDIA 的 CUDA Programming Guide。
v!#koqd1y.
在找到支持 CUDA 1.0 以上的装置之后,就可以呼叫 cudaSetDevice 函式,把它设为目前要使用的装置。
8peK[sz
最后是 main 函式。在 main 函式中我们直接呼叫刚才的 InitCUDA 函式,并显示适当的讯息:
FAq9G-\B
int main()
ZQyX zERp
{
|<#yXSi
if(!InitCUDA()) {
j2oU1' b
return 0;
'`&b1Rc
}
5k;}I|rg %
SqVh\Nn
printf("CUDA initialized.\n");
0U!_ o2]
e"ClG/M_XS
return 0;
]?_V+F
}
A27!I+M
这样就可以利用 nvcc 来 compile 这个程序了。使用 Visual Studio 的话,若按照先前的设定方式,可以直接 Build Project 并执行。
s6!! ty;Y
nvcc 是 CUDA 的 compile 工具,它会将 .cu 檔拆解出在 GPU 上执行的部份,及在 host 上执行的部份,并呼叫适当的程序进行 compile 动作。在 GPU 执行的部份会透过 NVIDIA 提供的 compiler 编译成中介码,而 host 执行的部份则会透过系统上的 C++ compiler 编译(在 Windows 上使用 Visual C++ 而在 Linux 上使用 gcc)。
=WK's8FB;8
编译后的程序,执行时如果系统上有支持 CUDA 的装置,应该会显示 CUDA initialized. 的讯息,否则会显示相关的错误讯息。
Y8/&1s_
cXNR<`
利用 CUDA 进行运算
d~y]7h |
EJ(z]M`f
到目前为止,我们的程序并没有做什么有用的工作。所以,现在我们加入一个简单的动作,就是把一大堆数字,计算出它的平方和。
!T.yv5ge'
首先,把程序最前面的 include 部份改成:
,Y@4d79
#include <stdio.h>
OpmPw4?}
#include <stdlib.h>
W(EN01d \
#include <cuda_runtime.h>
EQX?Zs?C
Kc`#~-`,(
#define DATA_SIZE 1048576
^2nH6,LPS
/}(\P@Z
int data[DATA_SIZE];
pwl7aC+6d
并加入一个新函式 GenerateNumbers:
6%&DJBU!
void GenerateNumbers(int *number, int size)
B-wF1!Jv
{
%F(lq*8X
for(int i = 0; i < size; i++) {
`82^!7 !
number
= rand() % 10;
)Ut9k
}
7b(r'b@N
}
j|VX6U
这个函式会产生一大堆 0 ~ 9 之间的随机数。
5s<.qDc
要利用 CUDA 进行计算之前,要先把数据复制到显卡内存中,才能让显示芯片使用。因此,需要取得一块适当大小的显卡内存,再把产生好的数据复制进去。在 main 函式中加入:
o`nJJ:Cxq-
GenerateNumbers(data, DATA_SIZE);
TlC??#
w}?,N
int* gpudata, *result;
OKnpG*)u=g
cudaMalloc((void**) &gpudata, sizeof(int) * DATA_SIZE);
HG3iK
cudaMalloc((void**) &result, sizeof(int));
fo e)_
cudaMemcpy(gpudata, data, sizeof(int) * DATA_SIZE,
oTveY
cudaMemcpyHostToDevice);
!3\( d{
上面这段程序会先呼叫 GenerateNumbers 产生随机数,并呼叫 cudaMalloc 取得一块显卡内存(result 则是用来存取计算结果,在稍后会用到),并透过 cudaMemcpy 将产生的随机数复制到显卡内存中。cudaMalloc 和 cudaMemcpy 的用法和一般的 malloc 及 memcpy 类似,不过 cudaMemcpy 则多出一个参数,指示复制内存的方向。在这里因为是从主内存复制到显卡内存,所以使用 cudaMemcpyHostToDevice。如果是从显卡内存到主内存,则使用 cudaMemcpyDeviceToHost。这在之后会用到。
X(Lz&fkd
接下来是要写在显示芯片上执行的程序。在 CUDA 中,在函式前面加上 __global__ 表示这个函式是要在显示芯片上执行的。因此,加入以下的函式:
"sed{?
__global__ static void sumOfSquares(int *num, int* result)
)Mh5q&ow
{
c LfPSA
int sum = 0;
Er|j\(jM
int i;
Q@rlqWgU ~
for(i = 0; i < DATA_SIZE; i++) {
-Zqw[2Q4
sum += num
* num
;
/EwNMU*6
}
nX<yB9bXDg
<<&SyP
*result = sum;
Fu65VLKh
}
.]/k#Hv
在显示芯片上执行的程序有一些限制,例如它不能有传回值。其它的限制会在之后提到。
Wv30;7~
接下来是要让 CUDA 执行这个函式。在 CUDA 中,要执行一个函式,使用以下的语法:
NZ-57Ji
函式名称<<<block 数目, thread 数目, shared memory 大小>>>(参数...);
@4>?Y=#
呼叫完后,还要把结果从显示芯片复制回主内存上。在 main 函式中加入以下的程序:
=91f26c!~
sumOfSquares<<<1, 1, 0>>>(gpudata, result);
+u3vKzD
r|3<UR%
int sum;
PUC:Pl77
cudaMemcpy(&sum, result, sizeof(int), cudaMemcpyDeviceToHost);
9L>ep&u)^
cudaFree(gpudata);
~/!jKH7`j
cudaFree(result);
9Yji34eDZ
5yf`3vV|3@
printf("sum: %d\n", sum);
v"dl6%D"
因为这个程序只使用一个 thread,所以 block 数目、thread 数目都是 1。我们也没有使用到任何 shared memory,所以设为 0。编译后执行,应该可以看到执行的结果。
^geY Ay
为了确定执行的结果正确,我们可以加上一段以 CPU 执行的程序代码,来验证结果:
MpJ]1
sum = 0;
8< z
for(int i = 0; i < DATA_SIZE; i++) {
/p)y!5e
sum += data
* data
;
]T%wRd5&-
}
zS `>65}e
printf("sum (CPU): %d\n", sum);
tY60~@YO&
编译后执行,确认两个结果相同。
3*e )D/lm
y7LM}dH#m
计算运行时间
rji<g>GQ
;JX2ebx
CUDA 提供了一个 clock 函式,可以取得目前的 timestamp,很适合用来判断一段程序执行所花费的时间(单位为 GPU 执行单元的频率)。这对程序的优化也相当有用。要在我们的程序中记录时间,把 sumOfSquares 函式改成:
o:@A% *jg
"MDy0Tj8EN
__global__ static void sumOfSquares(int *num, int* result,
v&xhS yZ
clock_t* time)
1r<'&f5
{
2lxA/.f
int sum = 0;
M aP -
int i;
[k=LX+w@
clock_t start = clock();
%Z yt;p2
for(i = 0; i < DATA_SIZE; i++) {
c
sum += num
* num
;
Ca3 {e1
}
UbP$WIrq
QPW+L*2
*result = sum;
Pk:b:(4
*time = clock() - start;
2 '8I/>-
}
DK<}q1xi
把 main 函式中间部份改成:
BWzo|isv
int* gpudata, *result;
QLZ%m $Z
clock_t* time;
{mA#'75a#
cudaMalloc((void**) &gpudata, sizeof(int) * DATA_SIZE);
1Ch0O__2L
cudaMalloc((void**) &result, sizeof(int));
W1[C/dDc
cudaMalloc((void**) &time, sizeof(clock_t));
l'?(4N
cudaMemcpy(gpudata, data, sizeof(int) * DATA_SIZE,
jNAboSf2Y
cudaMemcpyHostToDevice);
'3B7F5uLx"
sumOfSquares<<<1, 1, 0>>>(gpudata, result, time);
lht :%Ts$
P]4@|u;=6[
int sum;
]"i^VVw
clock_t time_used;
z[0t%]7l
cudaMemcpy(&sum, result, sizeof(int), cudaMemcpyDeviceToHost);
]gVW&3ZW
cudaMemcpy(&time_used, time, sizeof(clock_t),
_:G>bU/^
cudaMemcpyDeviceToHost);
Cil1wFBb
cudaFree(gpudata);
[-1Yyy1}
cudaFree(result);
$ ~/x;z:
8[IR;gZf
printf("sum: %d time: %d\n", sum, time_used);
6^TWY[z2%
编译后执行,就可以看到执行所花费的时间了。
nW ]T-!
如果计算实际运行时间的话,可能会注意到它的执行效率并不好。这是因为我们的程序并没有利用到 CUDA 的主要的优势,即并行化执行。在下一段文章中,会讨论如何进行优化的动作。
Xnxb.{C
改良第一个 CUDA程序
?IR+OCAA
在上一篇文章中,我们做了一个计算一大堆数字的平方和的程序。不过,我们也提到这个程序的执行效率并不理想。当然,实际上来说,如果只是要做计算平方和的动作,用 CPU 做会比用 GPU 快得多。这是因为平方和的计算并不需要太多运算能力,所以几乎都是被内存带宽所限制。因此,光是把数据复制到显卡内存上的这个动作,所需要的时间,可能已经和直接在 CPU 上进行计算差不多了。
NZuylQ)0
不过,如果进行平方和的计算,只是一个更复杂的计算过程的一部份的话,那么当然在 GPU 上计算还是有它的好处的。而且,如果数据已经在显卡内存上(例如在 GPU 上透过某种算法产生),那么,使用 GPU 进行这样的运算,还是会比较快的。
/ 16 r_l
刚才也提到了,由于这个计算的主要瓶颈是内存带宽,所以,理论上显卡的内存带宽是相当大的。这里我们就来看看,倒底我们的第一个程序,能利用到多少内存带宽。
RYM[{]4b5F
d4LH`@SUZ-
程序的并行化
{QT:1U\.
/%P,y+<}iG
我们的第一个程序,并没有利用到任何并行化的功能。整个程序只有一个 thread。在 GeForce 8800GT 上面,在 GPU 上执行的部份(称为 "kernel")大约花费 640M 个频率。GeForce 8800GT 的执行单元的频率是 1.5GHz,因此这表示它花费了约 0.43 秒的时间。1M 个 32 bits 数字的数据量是 4MB,因此,这个程序实际上使用的内存带宽,只有 9.3MB/s 左右!这是非常糟糕的表现。
r?WOum
为什么会有这样差的表现呢?这是因为 GPU 的架构特性所造成的。在 CUDA 中,一般的数据复制到的显卡内存的部份,称为 global memory。这些内存是没有 cache 的,而且,存取 global memory 所需要的时间(即 latency)是非常长的,通常是数百个 cycles。由于我们的程序只有一个 thread,所以每次它读取 global memory 的内容,就要等到实际读取到数据、累加到 sum 之后,才能进行下一步。这就是为什么它的表现会这么的差。
2~@Cj@P]
由于 global memory 并没有 cache,所以要避开巨大的 latency 的方法,就是要利用大量的 threads。假设现在有大量的 threads 在同时执行,那么当一个 thread 读取内存,开始等待结果的时候,GPU 就可以立刻切换到下一个 thread,并读取下一个内存位置。因此,理想上当 thread 的数目够多的时候,就可以完全把 global memory 的巨大 latency 隐藏起来了。
`%=!_|
要怎么把计算平方和的程序并行化呢?最简单的方法,似乎就是把数字分成若干组,把各组数字分别计算平方和后,最后再把每组的和加总起来就可以了。一开始,我们可以把最后加总的动作,由 CPU 来进行。
R'aA\k-
首先,在 first_cuda.cu 中,在 #define DATA_SIZE 的后面增加一个 #define,设定 thread 的数目:
5-"aK~@+
#define DATA_SIZE 1048576
u7 {R; QKw
#define THREAD_NUM 256
$lF\FC
接着,把 kernel 程序改成:
B`|H}KU
__global__ static void sumOfSquares(int *num, int* result,
HG)h,&nc-
clock_t* time)
!D9V9p
{
IKnXtydeI}
const int tid = threadIdx.x;
\5F {MBx !
const int size = DATA_SIZE / THREAD_NUM;
}nWW`:t kx
int sum = 0;
nk+9J#Gs
int i;
8[u$CTl7a
clock_t start;
&FDWlrGg
if(tid == 0) start = clock();
{?]&