《CUDA C编程权威指南》——3.5 展开循环

3.5 展开循环

循环展开是一个尝试通过减少分支出现的频率和循环维护指令来优化循环的技术。在循环展开中,循环主体在代码中要多次被编写,而不是只编写一次循环主体再使用另一个循环来反复执行的。任何的封闭循环可将它的迭代次数减少或完全删除。循环体的复制数量被称为循环展开因子,迭代次数就变为了原始循环迭代次数除以循环展开因子。在顺序数组中,当循环的迭代次数在循环执行之前就已经知道时,循环展开是最有效提升性能的方法。考虑下面的代码:

如果重复操作一次循环体,迭代次数能减少到原始循环的一半:

从高级语言层面上来看,循环展开使性能提高的原因可能不是显而易见的。这种提升来自于编译器执行循环展开时低级指令的改进和优化。例如,在前面循环展开的例子中,条件i< 100只检查了50次,而在原来的循环中则检查了100次。另外,因为在每个循环中每个语句的读和写都是独立的,所以CPU可以同时发出内存操作。

在CUDA中,循环展开的意义非常重大。我们的目标仍然是相同的:通过减少指令消耗和增加更多的独立调度指令来提高性能。因此,更多的并发操作被添加到流水线上,以产生更高的指令和内存带宽。这为线程束调度器提供更多符合条件的线程束,它们可以帮助隐藏指令或内存延迟。

3.5.1 展开的归约

你可能会注意到,在reduceInterleaved核函数中每个线程块只处理一部分数据,这些数据可以被认为是一个数据块。如果用一个线程块手动展开两个数据块的处理,会怎么样?以下的核函数是reduceInterleaved核函数的修正版:每个线程块汇总了来自两个数据块的数据。这是一个循环分区(在第1章中已介绍)的例子,每个线程作用于多个数据块,并处理每个数据块的一个元素:


注意要在核函数的开头添加的下述语句。在这里,每个线程都添加一个来自于相邻数据块的元素。从概念上来讲,可以把它作为归约循环的一个迭代,此循环可在数据块间归约:

如下所示,全局数组索引被相应地调整,因为只需要一半的线程块来处理相同的数据集。请注意,这也意味着对于相同大小的数据集,向设备显示的线程束和线程块级别的并行性更低。图3-25所示为每个线程的数据访问。

向主函数添加下面的代码,调用新的核函数:

因为现在每个线程块处理两个数据块,我们需要调整内核的执行配置,将网格大小减小至一半:

现在编译和运行这些代码,出现以下结果:

即使只进行简单的更改,现在核函数的执行速度比原来快3.42倍。可以进一步展开以产生更好的性能吗?reduceInteger.cu文件包含着展开的核函数中其他的两个实现,如下所示:

相应的结果概括如下:

正如预想的一样,在一个线程中有更多的独立内存加载/存储操作会产生更好的性能,因为内存延迟可以更好地被隐藏起来。可以使用设备内存读取吞吐量指标,以确定这就是性能提高的原因:

结果总结如下,归约的展开测试用例和设备读吞吐量之间是成正比的:

3.5.2 展开线程的归约

__syncthreads是用于块内同步的。在归约核函数中,它用来确保在线程进入下一轮之前,每一轮中所有线程已经将局部结果写入全局内存中了。

然而,要细想一下只剩下32个或更少线程(即一个线程束)的情况。因为线程束的执行是SIMT(单指令多线程)的,每条指令之后有隐式的线程束内同步过程。因此,归约循环的最后6个迭代可以用下述语句来展开:

这个线程束的展开避免了执行循环控制和线程同步逻辑。

注意变量vmem是和volatile修饰符一起被声明的,它告诉编译器每次赋值时必须将vmem[tid]的值存回全局内存中。如果省略了volatile修饰符,这段代码将不能正常工作,因为编译器或缓存可能对全局或共享内存优化读写。如果位于全局或共享内存中的变量有volatile修饰符,编译器会假定其值可以被其他线程在任何时间修改或使用。因此,任何参考volatile修饰符的变量强制直接读或写内存,而不是简单地读写缓存或寄存器。

基于reduceUnrolling8,线程束的展开可以添加到归约核函数中,如下所示:


因为在这个实现中,每个线程处理8个数据块,调用这个内核的同时它的网格尺寸减小到1/8:

这个核函数的执行时间比reduceUnrolling8快1.05倍,比原来的核函数reduceNeigh-bored快8.65倍:

使用下面的命令,stall_sync指标可以用来证实,由于__syncthreads的同步,更少的线程束发生阻塞:

结果总结如下。通过展开最后的线程束,百分比几乎减半了,这表明__syncthreads能减少新的核函数中的阻塞。

3.5.3 完全展开的归约

如果编译时已知一个循环中的迭代次数,就可以把循环完全展开。因为在Fermi或Kepler架构中,每个块的最大线程数都是1 024(参见表3-2),并且在这些归约核函数中循环迭代次数是基于一个线程块维度的,所以完全展开归约循环是可能的:


用以下执行配置调用这个核函数:

内核时间再次有了小小的改善,它的执行比reduceUnrollWarps8快1.06倍,比原来的实现快9.16倍:

3.5.4 模板函数的归约

虽然可以手动展开循环,但是使用模板函数有助于进一步减少分支消耗。在设备函数上CUDA支持模板参数。如下所示,可以指定块的大小作为模板函数的参数:


相比reduceCompleteUnrollWarps8,唯一的区别是使用了模板参数替换了块大小。检查块大小的if语句将在编译时被评估,如果这一条件为false,那么编译时它将会被删除,使得内循环更有效率。例如,在线程块大小为256的情况下调用这个核函数,下述语句将永远是false:

编译器会自动从执行内核中移除它。

该核函数一定要在switch-case结构中被调用。这允许编译器为特定的线程块大小自动优化代码,但这也意味着它只对在特定块大小下启动reduceCompleteUnroll有效:


注意,最大的相对性能增益是通过reduceUnrolling8核函数获得的,在这个函数之中每个线程在归约前处理8个数据块。有了8个独立的内存访问,可以更好地让内存带宽饱和及隐藏加载/存储延迟。可以使用以下命令检测内存加载/存储效率指标:

表3-6总结了所有核函数的结果。在第4章,将会更加详细地介绍全局内存访问,并且会对内存访问如何影响内核性能有更深的了解。

时间: 2024-09-20 00:38:54

《CUDA C编程权威指南》——3.5 展开循环的相关文章

《CUDA C编程权威指南》——3.5节展开循环

3.5 展开循环循环展开是一个尝试通过减少分支出现的频率和循环维护指令来优化循环的技术.在循环展开中,循环主体在代码中要多次被编写,而不是只编写一次循环主体再使用另一个循环来反复执行的.任何的封闭循环可将它的迭代次数减少或完全删除.循环体的复制数量被称为循环展开因子,迭代次数就变为了原始循环迭代次数除以循环展开因子.在顺序数组中,当循环的迭代次数在循环执行之前就已经知道时,循环展开是最有效提升性能的方法.

《CUDA C编程权威指南》——导读

###前 言 欢迎来到用CUDA C进行异构并行编程的奇妙世界! 现代的异构系统正朝一个充满无限计算可能性的未来发展.异构计算正在不断被应用到新的计算领域-从科学到数据库,再到机器学习的方方面面.编程的未来将是异构并行编程的天下! 本书将引领你通过使用CUDA平台.CUDA工具包和CUDA C语言快速上手GPU(图形处理单元)计算.本书中设置的范例与练习也将带你快速了解CUDA的专业知识,助你早日达到专业水平! 目 录 第1章 基于CUDA的异构并行计算 1.1 并行计算 1.1.1 串行编程和

《CUDA C编程权威指南》——1.2 异构计算

1.2 异构计算 最初,计算机只包含用来运行编程任务的中央处理器(CPU).近年来,高性能计算领域中的主流计算机不断添加了其他处理元素,其中最主要的就是GPU.GPU最初是被设计用来专门处理并行图形计算问题的,随着时间的推移,GPU已经成了更强大且更广义的处理器,在执行大规模并行计算中有着优越的性能和很高的效率. CPU和GPU是两个独立的处理器,它们通过单个计算节点中的PCI-Express总线相连.在这种典型的架构中,GPU指的是离散的设备从同构系统到异构系统的转变是高性能计算史上的一个里程

《CUDA C编程权威指南》——3.1节CUDA执行模型概述

3.1 CUDA执行模型概述 一般来说,执行模型会提供一个操作视图,说明如何在特定的计算架构上执行指令.CUDA执行模型揭示了GPU并行架构的抽象视图,使我们能够据此分析线程的并发.在第2章里,已经介绍了CUDA编程模型中两个主要的抽象概念:内存层次结构和线程层次结构.它们能够控制大规模并行GPU.因此,CUDA执行模型能够提供有助于在指令吞吐量和内存访问方面编写高效代码的见解. 在本章会重点介绍指令吞吐量,在第4章和第5章里会介绍更多的关于高效内存访问的内容.3.1.1 GPU架构概述 GPU

《CUDA C编程权威指南》——1.2节异构计算

1.2 异构计算 最初,计算机只包含用来运行编程任务的中央处理器(CPU).近年来,高性能计算领域中的主流计算机不断添加了其他处理元素,其中最主要的就是GPU.GPU最初是被设计用来专门处理并行图形计算问题的,随着时间的推移,GPU已经成了更强大且更广义的处理器,在执行大规模并行计算中有着优越的性能和很高的效率. CPU和GPU是两个独立的处理器,它们通过单个计算节点中的PCI-Express总线相连.在这种典型的架构中,GPU指的是离散的设备从同构系统到异构系统的转变是高性能计算史上的一个里程

《CUDA C编程权威指南》——1.4 使用CUDA C编程难吗

1.4 使用CUDA C编程难吗 CPU编程和GPU编程的主要区别是程序员对GPU架构的熟悉程度.用并行思维进行思考并对GPU架构有了基本的了解,会使你编写规模达到成百上千个核的并行程序,如同写串行程序一样简单. 如果你想编写一个像并行程序一样高效的代码,那么你需要对CPU架构有基本的了解.例如,数据局部性在并行编程中是一个非常重要的概念.数据局部性指的是数据重用,以降低内存访问的延迟.数据局部性有两种基本类型.时间局部性是指在相对较短的时间段内数据和/或资源的重用.空间局部性是指在相对较接近的

《CUDA C编程权威指南》——1.4节使用CUDA C编程难吗

1.4 使用CUDA C编程难吗CPU编程和GPU编程的主要区别是程序员对GPU架构的熟悉程度.用并行思维进行思考并对GPU架构有了基本的了解,会使你编写规模达到成百上千个核的并行程序,如同写串行程序一样简单.如果你想编写一个像并行程序一样高效的代码,那么你需要对CPU架构有基本的了解.例如,数据局部性在并行编程中是一个非常重要的概念.数据局部性指的是数据重用,以降低内存访问的延迟.数据局部性有两种基本类型.时间局部性是指在相对较短的时间段内数据和/或资源的重用.空间局部性是指在相对较接近的存储

《CUDA C编程权威指南》——3.8 习题

3.8 习题 1.当在CUDA中展开循环.数据块或线程束时,可以提高性能的两个主要原因是什么?解释每种展开是如何提升指令吞吐量的. 2.参考核函数reduceUnrolling8和实现核函数reduceUnrolling16,在这个函数中每个线程处理16个数据块.将该函数的性能与reduceUnrolling8内核性能进行比较,通过nvprof使用合适的指标与事件来解释性能差异. 3.参考核函数reduceUnrolling8,替换以下的代码段: 比较每次的性能并解释使用nvprof指标的差异.

《CUDA C编程权威指南》——3.8节习题

3.8 习题1.当在CUDA中展开循环.数据块或线程束时,可以提高性能的两个主要原因是什么?解释每种展开是如何提升指令吞吐量的.2.参考核函数reduceUnrolling8和实现核函数reduceUnrolling16,在这个函数中每个线程处理16个数据块.将该函数的性能与reduceUnrolling8内核性能进行比较,通过nvprof使用合适的指标与事件来解释性能差异.3.参考核函数reduceUnrolling8,替换以下的代码段: 比较每次的性能并解释使用nvprof指标的差异.4.参