《OpenACC并行程序设计:性能优化实践指南》一 1.1 简单的数据并行循环

1.1 简单的数据并行循环

在顺序处理器程序设计中,需要编写计算某个最终结果所需要的任务和数据操作的程序。通过创建OpenACC,编程人员可以插入编译指令给编译器提供信息,而这些编译指令是关于并行机会和数据在加速器与主机间来回传输的信息。结合编译器,程序员使用注记来创建、调试和优化并行代码,使得程序达到高性能。
OpenACC帮助程序员编写高效的数据和任务并行软件。
数据并行关注跨多个并发执行线程的分布式数据操作。在计算机科学中,线程是串行执行一段代码的线程的缩写。通过使用多个线程,应用程序可以使用并行硬件,例如多核处理器和大规模并行GPU。通过多个线程并发执行,这些并行硬件使得应用程序运行得更快,同时意味着单位时间内可以执行更多任务。
类似地,任务并行关注跨多个并发执行线程的分布式计算任务。同时执行多个任务,也可以让应用程序运行得更快。
如图1-1所示,C++示例accFill_ex1.cpp展示了数据并行,程序动态分配数组status,并且给数组赋值。

这个示例遵循了良好的编程习惯—检查用户输入:确定数组大小大于0。对赋值后的结果进行检查,确保程序退出时数组status中的元素确实成功被赋值为1。通过调用assert()来执行实际的检查,如果条件(sum==nCount)为假将会产生一个核心转储。同时也应该注意在设备并行区域不能调用assert,因为不允许提前退出。
写程序时使用assert()是帮助验证程序正确性的好方法,在调试模式下,断言(assertion)可以很快找到程序错误。调试完成后,在编译时定义NDEBUG来移除断言。因此,一旦调试完成后,assert()不会有任何的运行时或内存消耗。
良好的编程习惯规定需要验证应用程序接口(API)调用是否成功。细心的读者会注意到程序没有检查status的动态内存分配成功与否。为简化代码,忽略了这一项。
“#pragma acc parallel loop copyout(status[0:nCount])”是OpenACC注记,这条注记把accFill_exl.cpp从一个串行程序转变为可能使用几千个并发线程执行的并行程序。
这条注记解释如下:
#pragma acc parallel loop。#pragma acc告诉编译器这是OpenACC注记。parallel关键字告诉编译器使用并行构件(construct)规则(相对应的是kernels构件),这会在1.1.1节讨论。loop子句告诉编译器希望并行化C++的for循环。OpenACC注记使用C++范围规则来定义注记应用于哪个代码块范围。这个例子中for循环内的代码将会被并行执行。
copyout(status[0:nCount])。这个子句告诉编译器在OpenACC设备上创建一个数组status,数组索引从0开始,有nCount个元素的数组status。执行完并行区域以后将会把这些元素从设备端拷贝回主机端,因此主机端status的值与设备端一致。
在这个例子中使用copyout()而不是copy(),是为了避免主机端向设备端拷贝不必要的未初始化的数据。
执行串行代码的OpenACC主机端与并行设备共享内存时,在特定的情况下并不需要编译器执行数据拷贝。例如,这个OpenACC示例运行在多核处理器上时并不需要数据拷贝。
如图1-2所示,使用The Portland Group(PGI)统一的二进制可以把源码编译、运行在中央处理器(CPU)和图形处理器(GPU)。统一的二进制包含多个设备的可执行文件。-Minfo=accel命令行参数告诉编译器输出循环并行化的加速器信息。如黑体所示,产生了Tesla和多核处理器的内核。使用tesla是因为nvidia关键字已经被弃用了。tesla可执行文件可以运行在所有的NVIDIA GPU上。本章使用PGI 16.5编译器。
如图1-3所示,程序在多核x86处理器和NVIDIA GPU上成功执行。使用统一的二进制时,PGI OpenACC运行时检查ACC_DEVICE_TYPE环境变量来确定使用什么设备。

1.1.1 OpenACC内核构件与并行构件对比

OpenACC有两种并行计算构件:之前accFill_ex1.cpp例子中使用的并行构件(parallel construct)和接下来将讨论的内核构件(kernels construct)。
简单来说,OpenACC中的并行构件告诉编译器在接下来区域内的所有操作是一个单独的并行操作,每个线程都将会执行这个并行操作。增加loop子句(如之前的示例)是告诉编译器尝试并且并行化线程中循环内所有的操作,这和OpenMP程序员期望的一样。以CUDA为例,并行构件转换为一个CUDA内核。
安全提示:常见的错误是忘记添加循环指令(例如,只指定#pragma acc parallel),这会错误地告诉编译器接下来范围内的所有操作将会并行执行,这意味着每个并行线程都会执行这个for循环。同样,在语法中也很容易忘记acc,导致循环根本不会并行化。上述是众多编译错误中的两个原因,说明了检查编译器信息和核实每个并行区域在设备上实际运行的重要性。
相反,内核构件能使编译器更灵活地为目标设备生成高效的并行代码,包含将多个循环组合为单个的并行内核或者创建多个并行内核。同时编译器负责确保并行化循环是安全的,这点与并行构件不同,并行构件告诉编译器这么做是安全的。这也意味着编译器会很小心,有时并行化特定循环时需要更多的信息。内核并行化处理有三个步骤:
1.确认可以并行执行循环。
2.将抽象的循环并行性映射到具体的设备并行性上(例如,线程可以运行在多核处理器或适当并行配置的GPU上)。
3.编译器生成并且优化实际的代码来实现选定的并行性映射。
接下来的示例accFill_ex2.cpp使用内核构件在OpenACC设备上并行执行赋值操作和求和计算。如图1-4所示,程序使用内核构件移除数组传输,所有的计算将在设备上执行。

注意这两个例子中C++代码的逻辑是一样的,只有OpenACC语法有差异。
accTask_ex1.cpp中为#pragma acc parallel loop opyout(status[0: nCount])
accTask_ex2.cpp中为#pragma acc kernels create(status[0:nCount])
另外,添加大括号“{”和“}”来定义内核构件的范围。使用create子句在Open-ACC设备上分配status数组空间。
第二个注记#pragam acc loop vector reduction(+:sum)指定了向量归约求和。
简单来说,当目标设备支持向量归约求和时,代码中指定的归约计算数组status中所有元素的和。更确切地说,reduction()子句带操作符(示例中为“+”)和一个或多个标量变量。示例使用sum变量。在OpenACC区域最后,并行结果与原有变量的值相结合。这也就是为什么在归约操作前sum必须初始化为某个值(示例中为0),否则将导致未定义的行为。
vector子句告诉OpenACC编译器可以利用向量并行。
向量指令使用硬件在同一时间内执行多个操作,因而它是另一种形式的并行。现代多核x86处理器的每个核可以向硬件向量单元(或多核向量单元)发送向量指令,使得同一时刻执行多个数据并行操作。例如,AVX-512向量指令集是x86处理器当前最长的向量指令集,每条指令调用可以处理16个并发单精度、32位浮点操作。结果是每个向量单元达到16倍的性能提升。充分利用高端处理器上所有核的向量单元,可以导致很大的性能增益。GPU使用SIMD(单指令多数据)指令来达到类似的性能增益效果,只是在硬件的一个warp(CUDA术语,32个线程的集合)中跨线程地进行向量化,而不是由程序员或编译器显式发送向量指令。
图1-5显示了在多核处理器上向量与并行编程的性能增益。

1.1.2 OpenACC并行的多种形式

OpenACC执行模型允许用户表达3个层次的并行:工作组(gang)、工作项(worker)和向量(vector)。
如图1-6所示,从顶部开始:
线程:核心并行概念,单个、串行执行的线程,可以运行任何有效的C、C++或Fortran代码。
工作项:可以以SIMD或向量方式一起执行的线程组合称为工作项。(CUDA程序员认为OpenACC中的一个工作项就是一个warp。)
向量:运行向量或SIMD指令时,向量使工作项线程步调一致地运行。
工作组:工作项的组合称为工作组。(CUDA程序员认为一个工作组就是一个CUDA线程块。)工作组彼此独立地执行。

1.1.3 accFill_ex2运行时结果

从下面的PGI编译器报告可以看出,源代码使用create()子句在设备上分配数据空间,主机与设备间不需要拷贝操作。编译器决定GPU和多核CPU设备上都并行化内核区域里的循环(见图1-7)。

接下来的命令显示代码在CPU和GPU上都成功执行。使用ACC_DEVICE_TYPE环境变量来指定运行时设备(见图1-8)。

时间: 2024-11-02 06:08:12

《OpenACC并行程序设计:性能优化实践指南》一 1.1 简单的数据并行循环的相关文章

《OpenACC并行程序设计:性能优化实践指南》一 1.6 控制并行资源

1.6 控制并行资源 accParaCounter.cpp中的嵌套循环结构可以用来控制循环并行性,因此可以控制并行资源消耗. 大多数并行编程人员遵循在系统中利用最大并行性来实现最高性能的方法. 大量线程背后的思路是给予并行调度器尽可能多的线程以供调度,从而最大化地利用所有计算资源.GPU编程人员喜欢用占有率(occupancy)作为线程并行度的度量.高占有率意味着调度器有更多的激活线程以供调用,因此有机会实现更高性能. 高占有率并不一定转为最快的应用性能.指令级并行(Instruction le

《OpenACC并行程序设计:性能优化实践指南》一 导读

前言 欢迎阅读本书,这是一本由浅入深的书籍,从初学者到高级开发人员,都可以通过本书了解OpenACC的相关知识.本书由世界各地的24位作者共同编著而成,他们在高度并行编程的教学和实践方面分享了自己的专业知识.书中的例子既有时效性又不会过时.每个章节都是自包含的,可用于自学,也可以作为课堂教学的一部分. 这是一本关于并行编程的书,不仅仅介绍OpenACC语法或从文档中收集的信息,更介绍了如何编写实际的.高性能的以及可移植的程序,这些程序可以运行在从CPU到GPU的大量设备上.具体而言,书中演示了使

《OpenACC并行程序设计:性能优化实践指南》一 1.4 并行执行和竞争条件

1.4 并行执行和竞争条件 OpenACC并行化for循环(Fortran中是do循环),因此循环内的代码使用并发硬件执行线程并行执行. 循环内的变量i似乎是顺序递增的,但实际上在这个for循环中使用多个i变量的线程可能同时并行执行,这可能有点令人困惑.OpenACC不保证线程执行的顺序,注意这点非常重要.实际上,甚至不可能假设单调性.例如,很有可能第nCount―1次迭代实际上先于第0次迭代执行完. OpenACC不保证线程执行的顺序,注意这点非常重要. 总之,OpenACC编程人员不能也不应

《OpenACC并行程序设计:性能优化实践指南》一 2.6 小结

2.6 小结 OpenACC是一种描述型并行编程模型.在本章中,通过一个测试函数的应用,使用了OpenACC的多种特性来描述并行度和数据操控,并针对特定平台对代码进行了优化.尽管使用的是PGI编译器和PGProf性能调试器,但类似的优化流程也是适用于任何支持OpenACC工具包的应用的. 1.获得应用程序的性能分析结果,辨识和挖掘代码中的可并行之处. 2.逐步向编译器描述代码中可挖掘出的并行性.如果主机端和设备端使用各自的存储器,这一步骤后获得的代码很可能会减速. 3.描述应用程序的数据移动.编

《OpenACC并行程序设计:性能优化实践指南》一 1.2 简单的任务并行示例

1.2 简单的任务并行示例 接下来的示例accTask.cpp演示如何在OpenACC设备上并行执行单个任务. 使用任务并行时必须小心,因为OpenACC设备可能有不利于发挥性能的性能约束.一般来说,最好所有的OpenACC线程在相同时刻执行相同的任务,这样并行任务可以很好地映射到GPU SIMD流多处理器和常见的多核处理器的向量单元.高级程序员希望使用OpenACC async()子句和流,这使他们能实现更复杂的任务并行.特别是,感兴趣的读者应该研究并行任务分解和依赖图.推荐阅读<并行计算导论

《OpenACC并行程序设计:性能优化实践指南》一 3.2 逐步性能提升

3.2 逐步性能提升 本书的示例表明,通过提交更多的活动和优化数据传输,使用性能分析驱动的开发可不断提升OpenACC应用程序的性能.图3-2列出了优化混合应用遵循的模式. 从应用准备开始性能优化周期,然后进行实际的性能测量,并对性能数据进行分析.基于这些数据,编程开发人员尝试减少性能问题,并重新开始整个过程.下面使用Score-P和Vampri讲解性能提升周期里的前三步,以CUDA加速粒子单元模拟为例,其中CUDA部分代码很容易用OpenACC实现代替来获得同样的结果.此外,还引入了各种优化步

《OpenACC并行程序设计:性能优化实践指南》一 第1章 从串行编程到并行编程

第1章 从串行编程到并行编程 Rob Farber TechEnablement.com CEO/创始人 本章主要向读者介绍OpenACC,演示如何使用OpenACC编写运行在多核CPU和类似GPU加速器上的可移植并行程序,并展示如何在CPU和GPU上编译和运行OpenACC程序. 阅读本章后,读者将会理解以下内容: 如何创建.编译和运行OpenACC应用程序. 高性能OpenACC编程的三个准则. 数据并行和任务并行编程的基本概念. 理解大O表示法和Amdahl定律. 竞争条件.原子操作,以及

《OpenACC并行程序设计:性能优化实践指南》一 3.9 增加GPU任务并行

3.9 增加GPU任务并行 在图3-10中,放大主机-设备的追踪数据,可以看出在一些内核启动和开始执行内核间有时间差.此外,因为同步主机与GPU间的数据拷贝,所以GPU依旧有时空闲.使用CUDA流引入异步GPU活动,来确保PIConGPU可以向GPU发送更多的任务,让GPU找出最好的处理方式.图3-11显示了使用CUDA流的结果.现在每个主机线程都使用CUDA流(每个GPU有5个流),一个流负责主机与GPU间的数据拷贝,剩下的流负责向GPU提交并发任务.为了在GPU上实现极高水平的并发性,PIC

《OpenACC并行程序设计:性能优化实践指南》一 1.5 无锁编程

1.5 无锁编程 互斥锁是用于同步进程或线程的常用机制,这些进程或线程需要访问并行程序中的一些共享资源.互斥锁就像它们名字所说的:如果一个线程锁住了资源,另一个线程希望访问它需要等待第一个线程解锁这个资源.一旦资源被解锁,第二个线程在处理这个资源时会一直锁住它.程序的线程必须遵守:一旦使用完共享资源尽快解锁,以保持程序执行流程. 由于OpenACC中没有锁,编程人员需要熟悉无锁编程和数据结构的概念.无锁方法保证至少一个执行该方法的线程的进展.可能存在某些线程可以被延迟的情况,但是保证至少一个线程