OpenCL实战

978-7-115-34734-3
作者: 【美】Matthew Scarpino
译者: 陈睿
编辑: 陈冀康

图书目录:

详情

本书是OpenCL的全面、实用的介绍,向开发者展示了如何构建自己的高性能应用程序。首先介绍OpenCL的概念,包括矢量计算、并发编程、多线程编程等等,然后引导读者按步骤学习,从简单的数据结构到复杂的函数。

图书摘要

版权信息

书名:OpenCL实战

ISBN:978-7-115-34734-3

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美] Matthew Scarpino

  译    陈 睿

  责任编辑 陈冀康

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Simplified Chinese-language edition copyright ©2014 by Posts & Telecom Press. All rights reserved.

Original English language edition, entitled OpenCL in Action, by Matthew Scarpino , published by Manning Publications Co., 209 Bruce Park Avenue, Greenwich, CT 06830. Copyright © 2012 by Manning Publications Co.

本书中文简体字版由Manning Publications Co.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

版权所有,侵权必究。


OpenCL(Open Computing Language)是第一个面向异构系统通用目的并行编程的开放式、免费标准,也是一个统一的编程环境。OpenCL当前已经广泛应用于各种并行环境和不同行业领域。

本书是OpenCL编程的实践指南,旨在介绍如何在实际的应用程序中使用OpenGL解决问题。全书共16章和4个附录。正文可以分为3个部分。第一部分是第1~10章,主要讨论的是OpenCL语言及其各项功能。第二部分是第11~14章,展示如何用OpenCL来处理高性能计算领域会经常碰到的大规模任务。最后一部分,包括15章和16章,展示如何用OpenCL来加速OpenGL应用程序的执行。附录A介绍了SDK并演示如何安装AMD和Nvidia所提供的SDK。附录B讨论了OpenGL和着色器程序开发的基础知识。附录C介绍如何安装和使用MinGW。附录D讨论了嵌入式OpenCL标准。

本书适合于需要在异构平台和并发环境下进行开发的专业人士阅读,要求读者有一定的C语言基础。本书也适合对OpenCL和高性能计算感兴趣的读者参考。


1997年的夏天,我被震惊了。除了能在自己的专业(微电子工程)做实习生,我所能找到的最好的工作就是在一个专门研究高速信号处理的实验室做事,负责用C和MPI(Message Passing Interface,消息传递接口)编程实现二维快速傅里叶变换(fast Fourier transform,FFT)。好消息是这个实验室有16台新的SPARC计算机,坏消息是我对MPI和FFT一无所知。

在当时还是新奇古怪的Amazon.com上,我购买了一些相关书籍,然后才费力弄清楚MPI的基本处理过程:应用程序将一系列指令部署到多台计算机上,每台计算机根据它的ID访问数据。在每台处理器完成相应的任务之后,将结果返回给ID为0的处理器。

又过了些日子,我才对MPI有了更进一步的认识(阻塞和非阻塞数据传输,同步和异步通信)。在编程学习过程中,我发现自己爱上了分布式计算。我喜欢16台庞然大物锁步处理数据,它们就像是同场竞赛的运动员。我感觉自己就像是一名编舞,悉心策划一出精彩的舞蹈演出,亦或是一名作曲家,为整个乐团创作交响乐。实习结束时,我已经能用MPI编写出多个版本的二维快速傅里叶变换了,但最后因为网络延时的关系,实验室的研究人员否决了这个不太可行的方案。

那个暑假之后,我就对高性能计算倾注了极大的热情,全心投身于数字信号处理器、FPGA(Field-Programmable Gate Arrays,现场可编程门阵列)以及Cell处理器(它可是Sony PS3的“大脑”)的应用之中。但我想说的是,所有这些都不能够替代用OpenCL对GPU(Graphics Processing Unit,图像处理单元)的编程体验。现在的超级计算机设计也已经说明,没有哪块CPU可以提供和GPU相并论的“计算处理能力/功耗”的性能。同时,也没有哪门语言能够像OpenCL一样,得到如此广泛的设备支持。

当AMD在2009年发布它的OpenCL开发工具时,我就已经爱上了OpenCL。不仅仅是因为OpenCL能提供新的数据类型(向量数据类型)以及众多的数学函数,还因为它在很多方面都和MPI很像。两个工具集都免费、开源,也都可以用C或C++来编写应用程序。同时,应用程序也都是将指令发送到多个设备上,其中处理单元根据ID来确定访问相应的数据。MPI和OpenCL也都允许使用相似的阻塞/非阻塞传输和同步/异步通信来进行数据传输。

OpenCL在高性能计算领域还很新,也许很多程序员都还不知道它的存在。因此,为了帮助扩大这门语言的影响,我决定编写OpenCL in Action这本书。我很享受写书的过程,也希望它能帮助初学者了解、认识OpenCL和分布式计算。

在2011年的夏天,我开始写作此书的时候,我不禁有种周而复始的感觉。昨天晚上,我将本书收官在第14章的FFT应用程序上。它让我回想起了和MPI打交道的日子,但我还是惊讶于这些年的技术变迁。1997年,我实验里的16台SPARC机器花了将近1分钟的时间才完成32k FFT的运算。而到了2011年,300美元的显卡就能在几秒之内完成上百万个数据点的FFT变换。

技术在变化,但是编程的乐趣不变。也许现如今,分布式计算的学习曲线越来越陡,但回报的甘饴,而非补偿,也胜过以往。


OpenCL是一个复杂的主题。即使是编写最简单的应用程序,开发人员也需要知道主机编程、设备编程以及主机和设备之间的数据传输机制。本书的目的就是要展示如何解决这些问题,以及如何在实际的应用程序应用它们。

本书的格式是辅导书风格,即每个概念后面都会跟有示例代码,来演示如何在应用程序中实现刚才所学到的知识。刚开始的应用程序都很简单,有些只是为了获取设备和数据结构的相关信息。但越到后面,代码会变得越复杂,同时也会对主机和目标设备有更全面的使用。在后面的章节中,重点就会从学习OpenCL的基本概念和工作机制,转移到如何用OpenCL来应对海量数据的高速处理的难题。

在写作本书时,我对读者的假定是,从未听说过OpenCL,对分布式计算和高性能计算也是一无所知。我将尽我所能,讲清楚诸如任务并行化以及SIMD(单指令多数据流)这样的概念。

但因为OpenCL API是基于C的,所以本书会假定读者对C的基本概念十分清楚。读者也需要对指针、数组以及像malloc和free等内存访问函数相当的熟悉,也最好清楚math库文件中的C函数,因为OpenCL内核函数中也有相似函数名和用法的函数。

OpenCL应用程序可以在不同类型的设备上运行,但最主要的优势是,它可以被用来对GPU进行编程。所以,为了理解本书的内容,你最好是有一块带显卡或是混合CPU-GPU设备(如AMD的Fusion)的计算机。

本书被分为三个部分。第一部分,包括第1~10章,主要讨论的是OpenCL语言和它的各个功能。第二部分,包括第11~14章,展示如何用OpenCL来处理高性能计算领域会经常碰到的大规模任务。最后一部分,包括15章和16章,展示如何用OpenCL来加速OpenGL应用程序的执行。

第一部分的设计是为了迎合从未编写过OpenCL程序的程序员的需要。第1章来引入OpenCL的话题,解释它是什么,从哪里来,以及基本的运行过程。第2章和第3章介绍如何编写在主机上运行的应用程序,第4章和第5章介绍如何编写在OpenCL兼容设备上运行的内核。第6章和第7章讨论主机编程和内核编程过程中会碰到的高级话题,确切地讲,第6章讨论的是图像处理,第7章讨论的是事件处理和同步化的问题。

第8章和第9章讨论的是从第2章到第5章所遇到的概念,但所用到的是不同的编程语言。第8章讨论如何用C++来编写主机/内核代码,第9章讨论如何用Java和Python来编写OpenCL应用程序。如果你不是必须要用C来编程,我强烈推荐你看看这几章中所介绍的工具集。

第10章是第1部分和第2部分的桥梁。它展示的是如何利用OpenCL的并行编程的优势,举的例子是实现简单的归并算法,将一百万个数据点加起来。同时,也提出了一些编写实际应用程序的指导原则。

第11章到第14章关注的是用OpenCL来处理海量数据,应用程序一般都是对百万个数据点进行操作。第11章讨论的是MapReduce算法的实现以及两种排序算法:双调排序和基数排序。第12章讨论的是稠密矩阵的运算,第13章讨论的是稀疏矩阵的运算。第14章讨论的则是如何使用OpenCL来实现快速傅里叶变换。

第15章和第16章是我个人最喜欢的,OpenCL的优势就在于可以用来加速三维渲染,这也是游戏开发和科学可视化最重要的话题。第15章讨论的是OpenCL-OpenGL互操作的问题,展示两个工具集如何共享和顶点属性相对应的数据。第16章将展开对OpenCL-OpenGL互操作话题的讨论,来了解如何用OpenCL加速OpenGL的纹理处理。这些章节都需要对OpenGL 3.3和着色器程序开发有所了解,而这两个话题的相关知识都被放在附录B中。

本书结尾的附录提供了很多关于OpenCL的有用信息,但这些资料并不是直接在一般的OpenCL开发中使用。附录A先是介绍SDK,然后演示如何安装AMD和Nvidia所提供的SDK。附录B讨论的是OpenGL和着色器程序开发的基础知识。附录C介绍的是如何安装和使用MinGW(Minimalist GNU for Windows),MinGW是一个类GNU环境,用于在Windows系统下编译生成可执行程序。最后,附录D讨论的是嵌入式OpenCL标准。

最后,代码才是王道。本书包含了超过60个OpenCL应用程序的代码,你可以到出版社的网站去下载源代码:www.manning.com/OpenCLinAction 或 www.manning.com/scarpino2/。

下载站点提供了一个指向包含源代码的压缩包,可以用基于GNU的编译工具来编译。这个压缩包中的文件夹和各章/附录相对应,每一个顶层文件夹中的子文件夹和示例项目对应。例如,如果查看Ch5/shuffle_test路径,你可以看到第5章shuffle_test项目的源代码。

至于依赖文件,每个项目都需要开发系统上已经安装了OpenCL库文件(Windows下是OpenCL.lib,*nix系统下是libOpenCL.so文件)。附录A所讨论的就是如何获取、安装相应的SDK。

此外,第6章和第16章讨论的是图像处理,这些章节的源代码用到了开源PNG库。第6章介绍了如何获取相应的库文件。附录B和第15、16章都涉及了对OpenGL的访问,附录B介绍了如何获取、安装这个工具集。

尽管听起来很懒惰,但我还是情愿复制、粘贴源代码到我的应用程序之中,而不是选择从头开始。这不仅节约时间,而且减少了代码输入的错误。本书中的所有代码都是公开的,所以你可以免费下载,复制、粘贴任何你想要的代码。但在此之前,还是有必要了解我所使用的命名约定规则。

金无足赤,人无完人。如果本书在某个主题上说得不够清楚,或者出现了错误,请你通过Manning的作者在线给我提反馈意见。本书的作者在线论坛网址是www.manning.com/OpenCLinAction,单击Author Online链接即可。

简单的问题和疑问,都会得到最及时的答复。但如果你是对双调排序实现的第402行代码不感冒,那我就得花些时间才能回复你了。我很愿意讨论所有和OpenCL相关的一般性问题,但如果你是想寻求复杂、特定应用的帮助,例如对定制FFT程序进行调试,我还是建议你找一个专业顾问来帮忙,这样会更有效一些。

OpenCL in Action的封面图的标题是“Kranjac”人,一群生活在Slovenian Alps,Carniola的原住民。插画来自Balthasar Hacquet的Images and Descriptions of Southwestern and Eastern Wenda, Illyrians, and Slavs,该书由克罗地亚Split的Ethnographic Museum在2008年重印出版。Hacquet (1739~1815)是一名奥地利医生、科学家,曾经在从意大利东北到斯洛文尼亚的Julian Alps(以Julius Caesar的名字命名)地区做过多年的植物学、地理学以及人类学的研究。这些手绘的插图都出现在Hacquet出版的科学论文和书籍中。

Hacquet所出版的书刊中插画异常丰富、多种多样的特点也说明了200年以前的东阿尔卑斯山脉地区的与众不同和相互独立。在那个年代里,各个村庄之间的穿着都相互有别,村民的穿着也反映出他的社会地位。但到了后来,原住民的穿着就发生了很大的变化,曾经是如此的丰富,引以为豪的差异化都随时间如流水一般,悄然逝去。而就像在当今社会,很难将世界不同角落的民众细细分开一样,斯洛文尼亚山脉那些曾经曾经与众不同的村民,也已经和其他地区的斯洛文尼亚居民,或其他的欧洲大陆居民一样,别无二致。

我们Manning希望通过以封面插图的形式,将两个世纪前的服装以插图的形式重现,来慨叹计算机工业的发明、创新及其所带来的趣味。


我是在2003年为Manning Publications撰写第一本书的,尽管世事变迁,他们却还是一如既往地致力于出版高品质的书籍。我非常感谢全体Manning员工的辛勤工作以及奉献精神,但也想在此特别感谢以下几个人。

首先,我想感谢的是Maria Townsley,她是我的开发编辑(developmental editor)。Maria是我见过的最关注实现结果的编辑之一,除此之外,她在书籍的组织和书写调理清晰方面也提供了很多的建议。尽管我这人脾气不好,但到最后也不得不服气,承认她确实是对的。此外,虽然,我不断地反复修改表格内容,她都一直鼎力地给予支持和帮助。

同时,我还想深深感谢整个Manning的发行团队。尤其是要感谢Andy Carroll,感谢他对编辑本书的巨大付出。他的意见和观点不仅对本书的润色起到了巨大的作用,同时他的专业知识也让本书更好地做到了深入浅出。同样地,我也想在这里感谢Maureen Spencer和Katie Tennant,感谢他们对最终稿的字斟句酌,感谢Gordan Salinovic,感谢他对本书图片和结构的不懈努力。同时,我还想感谢Mary Piergies,感谢她策划了全书,保证了本书能够维系Manning的高品质。

简单而言,Jörn Dinkla是我合作过的最好的技术编辑。我已经将本书中的示例代码在Linux和Mac OS下分别进行了测试,而他更进一步,将代码用AMD和Nvidia的SDK在Linux下进行了测试。他不仅找出了我未曾发现的几个错误,而且很多次地花时间研究出错的个中原因。我不敢想象,如果缺少他的帮助,事情会演变成什么样子。在此,我深深感谢他的辛勤付出,对提升本书代码质量所作出的努力。

我还要感谢Candace Gilhooley,感谢他对本书的宣传。由于OpenCL还很年轻,它的读者群并不像Manning的众多Java书一样,族群丰富。但不得不说,无论是在网络专栏、图书展览还是会议研讨中,Candace已然对OpenCL in Action做足了宣传。

Manning最强大的地方就在于它一直信赖反馈意见。在本书的编写和编辑期间,Karen Tegtmeyer和Ozren Harlovic邀请了众多的审稿人,并组织了多轮的审阅。正是由于这些审稿人的反馈意见,这本书得以融入了很多我不大可能会考虑的重要主题,他们是:Olivier Chafik, Martin Beckett, Benjamin Ducke, Alan Commike, Nathan Levesque, David Strong, Seth Price, John J. Ryan III 和 John Griffin。

最后,我还要感谢Jandex Indexing的Jan Bednarczuk,感谢她对索引本书内容的细致工作。她不仅在很短的时间创建了一个完整、专业的检索表,还发现了不少的书写错误。再次感谢。


第一部分展示的是OpenCL语言,我们将详细讨论OpenCL的数据结构和函数,并通过例子来了解它们在应用程序中的作用。

第1章先介绍OpenCL有何用处以及工作原理。第2章和第3章将介绍如何编写主机应用程序,第4章和第5章将讨论内核编程,第6章和第7章将讨论OpenCL编程的高级话题,图像处理和事件处理。

第8章和第9章将讨论如何用C以外的语言(例如C++、Java和Python)来编写OpenCL程序。最后,第10章将介绍如何用OpenCL来开发大型应用程序。


本章所含内容

2010年的10月,高性能计算领域诞生了一项革命性的作品。由中国国家超级计算天津中心建造的Tianhe-1A终于走出迷雾,站在了世界高性能计算机之巅。Tianhe-1A的最高计算速度可达2566TFLOPS(trillion floating-point operations per second, 兆亿浮点指令每秒),而这比第二名的Cray’s Jaguar超级计算机足足快了一半。表1.1列出了世界前三名的超级计算机。

表1.1 2010年超级计算机界的前三名(来源:www.top500.org)

超级计算机名

最快速度(TFLOPS)

处理器

功耗(kW)

Tianhe-1A

2566

14336 Intel Xeon CPUs
7168 Nvidia Tesla GPUs

4040.00

Jaguar

1759

224256 AMD Opteron CPUs

6950.60

Nebulae

1271

9280 Intel Xeon CPUs
4640 Nvidia Tesla GPUs

2580.00

为什么在Tianhe-1A和Nebulae中出现的GPU会带来如此革命性的不同?要知道在2009年,世界上的前三名可是都没有包含GPU的,放眼前20名,也只有一台机器里面有GPU。而这张表清晰地表明配有GPU的两套系统不仅仅是在计算性能上有巨大的优势,而且在能耗上也相当经济。

使用GPU完成非图像程序,这也被称为通用GPU运算general-purpose GPU computing),或者说GPGPU运算。在2010年以前,GPGPU运算还只是高性能计算界的新鲜玩意,并不受待见。但到了今天,工程师和科研人员都得出了统一的结论,CPU/GPU系统昭示着未来超级计算发展的方向。

现在的难题是:如何对这些混合架构的设备(hybrid device[1])编写程序?传统C或C++的编译目标一般都是CPU。Cray的专用语言Chapel和Cray的汇编语言(Cray Assembly Language CAL)也都秉承了这一理念。而反观Nvidia的CUDA(Compute Unified Device Architecture),它只能针对Nvidia的GPU,而非CPU,进行程序编写。

OpenCL(Open Computing Language)便是解决良策。OpenCL例程不仅可以在诸如AMD、Nvidia、Intel这些主流芯片厂商的GPU或CPU上执行,甚至还可以在Sony的PS3上运行。OpenCL不是专用语言,它是一项开放标准,因此你可以随意下载开发工具套件。当你在用OpenCL编程时,你根本不必担心芯片厂商或是计算内核的多少。你的代码成功编译后,不仅可以在AMD最新的Fusion芯片上执行,也可以在Intel Core芯片或是Nvidia的Fermi芯片,亦或是IBM的Cell Broadband Engine芯片上看到结果。

本书的目标就是要展示如何编写跨平台架构的应用程序,以及最大可能地利用硬件资源。然而在这一章只是要先完成对OpenCL语言的大致介绍。先从对OpenCL的优势和运行过程的了解开始,然后再编写一个简单而完整的应用程序。但在开始之前,我们还是有必要先来了解OpenCL的诞生过程。各个公司都花费了很大的精力来设计这门语言,一旦你理清了头绪,也就明白为什么我们有必要介绍OpenCL的“前世今生”了。

x86架构在个人计算领域已经占有了绝对的统治性地位,但是在图像或是高性能计算领域,却没有哪个体系结构能够称王称霸。尽管像Nvidia的Fermi,或是AMD的Evergreen,亦或是IBM的Cell Broadband Engine等这些芯片的体系结构的目的都基本相同,但你却很难在它们之间找出共通之处。每个芯片都有自己的指令集,而且在OpenCL出现之前,如果想要在这三种不同的芯片上编程,你就必须要掌握三门不同的编程语言。

而在Apple,这帮隐士一般的理想主义者创造出了一系列难以置信的大受欢迎的消费电子产品,诸如iPhone、iPad、iPod以及Mac系列的个人电脑。但是,Apple并没有针对Mac个人电脑设计单独的芯片;而是选择从其他芯片制造商的产品中进行选型设计。如果Apple选择公司A的图像处理芯片用作它的新开发,则未来公司A的市场份额和对开发人员的吸引力便将会有一个大幅度的攀升。这也是每个芯片企业都唯Apple马首是瞻的根本原因。

2008年,Apple向芯片制造商抛出了这样一个问题:“为什么我们不搞一个通用接口,这样开发人员便省去了学习语言的时间,而能更加专注于对设备的编程?”这问题要是换做他人提出,像Nvidia、AMD、Intel和IBM这样的冷酷的开发商便会不置可否地嘲笑这个想法。但是没人敢笑Apple。虽然工期很赶,但是头头脑脑们还是凑时间碰面,在当年结束前便制订出了OpenCL的第一版草稿。

为了控制OpenCL的进度和开发,Apple和它的朋友们又成立了OpenCL工作组。它是Khronos Group众多工作组中的一个,而Khronos Group则是由多个公司组成的联盟,目的是为了推进图像多媒体的发展。成立之初至今,OpenCL 工作组已经发表了两个版本的标准。OpenCL v1.0发布于2008年,而OpenCL v1.1发布于2010年,最近的OpenCL v2.0预计将于2012年发布。

这一小节我们讨论了什么商业界会如此欢迎OpenCL,但你若对这门技术还持观望态度,我也很能理解这种想法。而下一节,我们将更深层次地讨论OpenCL的技术优点。希望通过相应的介绍,你能对OpenCL较之于传统编程语言的优势有更进一步的了解。

你可能听说过这样的表述,OpenCL指的是一门独立的语言,但其实,这种表述其实并不准确。OpenCL标准只是在C和C++的基础之上,扩展定义了一些数据类型,数据结构以及函数罢了。尽管开发人员已经针对Java和Python设计了一系列的OpenCL接口库,但标准中只要求OpenCL框架提供C和C++编写的API。

OpenCL和多核计算史上的重大事件如下:

2001-IBM发布POWER4,第一块多核处理器;

2005-第一批针对桌面电脑的多核处理器发布:AMD的Athlon64 X2和Intel的Pentium D;

2008年6月-OpenCL工作组形成,加入Khronos Group大家庭;

2008年12月-OpenCL工作组发布了OpenCL v1.0的规格标准;

2009年4月-Nvidia发布了针对Nvidia显卡的OpenCL SDK;

2009年8月–ATI(现在的AMD)发布了针对ATI显卡的OpenCL SDK。Apple在Mac OS 10.6(雪豹版)中加入了对OpenCL的支持;

2010年6月-OpenCL工作组发布了OpenCL v1.1的规格标准。

这里有个很棘手的问题:有什么是你能靠OpenCL完成,而一般的C和C++编程却束手无策?而这正是我要用整本书来完整解释、回答的问题,但是首先,我们还需要考察OpenCL的三个主要优势:可移植性,标准化的向量处理,以及并行编程。

Java是世界上最为著名的编程语言之一,而它的成功很大程度上应当归功于它的那句格言:“一次编写,各处运行(Write once,run everywhere)[2]”。有了Java,你并不需要针对不同的操作系统重写自己的代码。只要操作系统兼容支持JVM(Java Virtual Machine,Java虚拟机),你的代码就能运行。

OpenCL秉承的是同样的理念,但那句格言还得改改才更适用,“一次编写,各设备上运行(Write once, run on anything)”。每个芯片厂商不仅提供兼容OpenCL的硬件,还提供开发工具编译OpenCL代码,支持其在硬件上的运行。换言之,你只需要一次编写OpenCL例程,便可以保证其在兼容芯片(多核处理器或是显卡)上的编译、运行。这便是相对于传统的高性能计算的巨大优势,你并不需要针对特定产商的硬件,而去学习它们的专用编程语言。

而OpenCL所带来的好处并不仅仅只是能够在任何兼容硬件上运行。OpenCL应用程序可以针对不同的设备完成一次编译,这些设备甚至都不需要有相同的体系结构,或是来自同一个厂商。只要他们的设备能够兼容OpenCL,编写的函数便能运行。而这是以往的C/C++编程所无法达到的,它们所编写的程序只能是在特定的编译目标上运行。

这里有一个实例。设想你有一块来自AMD的多核处理器芯片,一块来自Nvidia的显卡以及一块来自IBM的通过PCI连接的加速器。一般而言,你根本不可能针对这三个不同的系统进行一次性的程序编写,而它们所要求的编译器和链接器也是各不相同。但是OpenCL程序所编写的可执行程序却可以在三种不同的硬件上运行。换言之,现在可以靠同样的程序在不同平台的硬件上完成相同的任务。如果你还有更多的兼容设备需要考虑,只需要重新编译程序即可,而不是重写所有的代码。

标准化的向量处理也是OpenCL的一大优势,但在我解释原因之前,还是需要对所讨论的问题稍加定义。这里的术语向量将贯穿本书始终,而它的表述也可分为如下三种(尽管三种表述本质上都差不多):

最后一种表述对OpenCL而言非常的重要,而高性能处理器也正是要完成这一操作。如果你有听说过超标量处理器或是矢量处理器的话,这也就是我们所讨论的设备类型。基本上所有的现代处理器都具备处理向量运算的能力,但ANSI C/C++并没有定义任何基本的向量数据类型。这听起来很奇怪,但确实是个显而易见的问题:向量指令基本上都是针对向量来的。Intel处理器使用的是SSE指令扩展,Nvidia芯片需要PTX指令集,而IBM的芯片则是依靠AaltiVec指令集来执行向量运算。这些指令集都没有任何共通之处[3]

但是有了OpenCL,你便可以实现“一次编写,各设备上运行”的想法。具体到不同平台的应用程序,Nvidia的OpenCL编译器会输出PTX指令的程序,而IBM的OpenCL编译器会输出AltiVec指令的程序。显然,如果你是要面向多个硬件平台编写高性能的应用程序,OpenCL绝对会节省大量的时间。在第4章中,我们将讨论OpenCL的矢量数据类型,而在第5章中,我们将看到具体有那些函数来负责向量操作。

如果你有过大规模的应用程序的编程经验,你肯定接触过并发(concurrency)[4]的概念——顺序代码通过任务调度在进程(或线程)间实现资源分享,并行执行。OpenCL包含了并发的特点,但更重要的是它带来了并行编程parallel programming)的可能。并行编程就是将不同的运算任务分配给多个不同的处理单元,同时并行执行。

在OpenCL之中,这些任务被称为内核。而内核是针对一个或多个兼容OpenCL的设备而特别编写的函数,并通过主机应用程序发送到相应的一个或多个设备上。主机应用程序就是一个在我们称为主机的用户开发系统上运行的C/C++应用程序。主机既可以选择将内核程序发送到计算机显卡上的GPU中运行,也可以选择当前的CPU作为执行目标。

主机应用程序通过名为上下文context)的容器来管理所连接的设备。图1.1所示的是主机与内核及设备之间的相互关系。

主机必须要从一个名为程序program)的内核容器中选择函数,才能创建出内核程序;然后,它再向内核函数中调入相应的参数数据,并发送到名为指令序列的数据结构中。指令序列是主机实现对设备控制的一种机制,而当内核入列时,设备便会执行相应的函数。

OpenCL应用程序可以配置不同的设备来完成不同的任务,每个任务都是对不同的数据进行运算处理。换言之,OpenCL提供的是任务并行的机制。这也是相比于其他并行编程工具集的重要优势,其他的开发工具只允许数据并行的处理机制。在数据并行的系统中,每个设备都收到相同的指令,但处理的是整个数据集的不同部分。

图1.1所示的是OpenCL在设备间完成任务并行处理的整个过程;这张图并没有给出设备内部的运行机制。大多数的OpenCL兼容设备所含的处理单元(processing element)往往不止一个,换言之,设备内部还存在并行处理的可能性。第3章我们更多地关注这种并行处理,以及如何通过数据划分来最大化地利用设备内部的并行处理。

图1.1 内核程序在OpenCL兼容设备中的分布

可移植性,向量运算以及并行编程使得OpenCL较之一般的C/C++更为强大、高效,但随之带来的也是计算系统更大的复杂性。在实际的OpenCL编程中,创建一系列不同的数据结构并协调它们之间的关系是必不可少的,尽管如此,要保持整个过程中任何事情都是明明了了,条理清楚,却并非易事,下一节我们将通过一个类比,让你对此有更深的认识。

在最开始学习OpenCL的时候,我经常会被那些奇怪的数据结构以及各式各样的术语所吓到。对于我而言,要记住这些术语所包含的内容以及相互间的关系并非易事,所以我采用了类比学习的方法,把OpenCL应用程序的运行过程比作打扑克牌。这看起来很奇怪,但个中缘由,由我慢慢道来。

扑克游戏的结构总是一张牌桌,一个庄家加上一个或多个玩家再配上一副纸牌。玩家之间并没有关系,他们只是向庄家要牌,或是增加赌注。庄家挨个询问玩家的请求,游戏结束之后,庄家便会查看手牌,判定胜负。

在这个类比中,庄家扮演的是OpenCL主机的角色,每个玩家则好比OpenCL兼容设备,而桌子就相当于是上下文,每副纸牌就相当于是一个内核程序。每个玩家的手牌就像是一个指令序列。表1.2更清楚地说明了纸牌游戏和OpenCL应用程序的运行过程间的类比关系。

表1.2 OpenCL运行过程和纸牌游戏的比较

纸牌游戏

OpenCL应用程序

庄家依桌坐定,确定玩家的身份

主机选择设备,并放入上下文之中

庄家派牌,每个玩家都有手牌

主机从程序中选择内核,并将其发送到设备的指令序列中

每个玩家查看手牌,决定相应的行动

每个设备运行指令序列中的内核程序

游戏过程中,庄家轮流询问玩家的请求

主机接收来自设备的事件,并调用事件处理例程

游戏结束,庄家查看每个玩家的手牌情况,判定胜负

所有设备完成运算处理之后,主机接受、处理结果输出

为了让这个类比更直观,图1.2所示的是有4个玩家时的卡牌游戏的情景,每个人的手牌是4张。我希望通过图1.1和图1.2,让这个类比更加的直观、易懂。

图1.2 纸牌游戏的形象演示

在下面的几个章节里,我们还会回过头来再考察这个类比,并补充新的内容。这个类比虽然能让我们可以更直观地了解OpenCL的整个运行过程,但还是有些问题,下面仅列举出我所认为的六点最严重的不足:

如果你还是不大适应OpenCL的术语,别担心,第二章我们会深入地透过代码来了解这些数据结构。毕竟用这些东西编出代码才是王道。下一节我们来看看OpenCL代码究竟是何模样。

现在,你应该对OpenCL的目标有了一个大致的了解。我还希望你能对OpenCL应用程序的运行过程有所认识。但如果想要更全面而深入地认识OpenCL,你还需要仔细阅读源代码。

这一节将看到两个OpenCL源代码文件,一个是针对主机,一个是针对设备。两个程序一起完成对4*4矩阵和4*1向量的矩阵乘积运算。这项操作对图像处理很重要。其中矩阵表示的是转换,而向量表示的是颜色或是空间中的点。图1.3所示的是矩阵-向量乘积的运算过程,以及运算结果。

图1.3 矩阵-向量的乘法运算

如果打开本书示例代码的路径,你会在Ch1文件夹中找到这两个源程序文件。第一个文件matvec.c是在主处理器上执行,它将创建一个内核,然后将其发送给找到的第一个设备。下面所示的主机上运行的程序代码。注意,源代码是用C语言编写的。

注意,下面的源文件代码已经略去了查错例程的内容,完整的代码参见本书的示例代码中的matvec.c文件。

程序1.1 创建和分配一个矩阵-向量乘法运算的内核:matvec.c

源文件虽长,但并不复杂。代码的大部分都是在创建OpenCL的数据结构,其中的命名约定如下:cl_context是前面讨论的上下文,cl_platform_id是前面讨论的平台,cl_device_id是前面讨论的设备等。了解这些命名约定之后,代码在主机程序间的复制就显得很简单。

不同的是,cl_program和cl_kernel的创建则因应用程序的不同而不同。程序1.1中,应用程序通过文件matvec.cl里的函数来创建内核程序。更准确地讲,主机将matvec.cl以字符的形式读到一个字符数组中,通过这个字符数组创建出程序,编译整个程序。然后,通过其中的函数matvec_mult完成对内核的创建。

文件matvec.cl中的内核代码比文件matvec.c中的主机代码短很多,而且一个函数matvec_mult便完成了整个矩阵-向量相乘的算法(见程序1.2)。

第2章和第3章我们将讨论如何编写出程序1.1那样的主机应用程序。第4章和第5章我们将讨论如何编写出程序1.2那样的内核函数。

如果你已经迫不及待地想编译这两个应用程序,实现点乘算法,我建议你先看看附录A,里面讨论了如何获取、安装OpenCL开发工具。在此之前,我觉得对OpenCL标准有一个概括性的认识还是很有必要的,我们将在下一节来展开讨论。

程序1.2 在设备上完成点乘算法的内核函数: matvec.cl

当你在通过网站www.khronos.org/opencl了解OpenCL时,你会看到一个名为opencl-1.1.pdf的重要文件。这其中包含了OpenCL1.1的标准,它所包含的是大量关于编程语言的内容。它不仅仅定义了OpenCL的函数和数据结构,还对厂商所提供的开发工具提出了功能上的要求。除此之外,它还设定了一系列的标准来判定设备的兼容性。

但是兼容软件和硬件所能提供的功能已经超出了标准所设定的内容。这些额外的特性需要通过扩展extensions)的方式供OpenCL应用程序使用。有两种主要类型的扩展:一种和厂商的软件包(叫做平台)相关,一种和设备相关。第2章将讨论如何编写代码来确认这些软件平台扩展和设备扩展。

每个OpenCL扩展都有特定的名字,而这也反映了它的兼容度。如果一个扩展为OpenCL工作组所认可,它的名字就会像cl_khr_<name>一样,如果它只是发布在开发商的SDK中,还没有得到工作组的认可,它的名字则形如cl_<vector>_<name>。

例如,在我的Linux中,AMD平台所支持的扩展便是cl_khr_icd。这个扩展就是软件平台扩展。而这也让开发工具能够找到安装在系统里,针对特定厂商的OpenCL库文件。ICD表示的是Installable Client Driver(可安装的用户驱动),附录A会更深入地讨论这个问题。

尽管文件matvec.c和 文件matvec.cl中的代码让人眼前一亮,但如果没有经过编译、运行,这些代码就更像花拳绣腿,没有任何实际的作用。而为了能够编译、运行,你就必须要有一套兼容OpenCL的开发工具。正如OpenCL标准所述,框架包含如下三个部分。

平台层——通过它来访问设备,设定上下文

运行时——让主机应用程序将内核和指令队列发送到上下文中的设备上

编译器——构建含有可执行内核的程序

OpenCL工作组并没有提供自己的设计框架。这就要求每个OpenCL兼容设备的生产商需要将OpenCL开发框架作为自己所发布的SDK的一部分。由Nvidia和AMD分别发布的OpenCL SDK是现在比较受欢迎的开发工具,两款SDK都免费,并且包含用于构建OpenCL应用程序所需的库函数文件和工具。不管是面向Nvidia或是AMD的设备进行开发设计,安装SDK的过程都很简单。附录A给出了整个安装的过程,并讨论了如何用这些SDK来构建应用程序。

OpenCL是一套针对高性能处理芯片构建并行程序的强大工具集。有了OpenCL,你不必费心学习针对设备的编程语言,一次编写程序,便可完成在所有的OpenCL兼容硬件上的运行。

除了可移植性,OpenCL的优势还在于向量处理和并行编程。在高性能计算中,向量是一种包含多个相同数据类型的数据结构。但是和其他的数据类型不同,当在向量上发生操作时,对向量中各个分量都是并行处理完成的。并行编程意味着,主机应用程序可以控制多个设备上的数据处理。OpenCL可以将不同的任务发送到不同的设备上,这种处理方式也被称为任务并行编程。如果能够得到有效利用,向量运算和任务并行编程能够提供较之于标量、单处理器系统在运算性能上的巨大提升。

OpenCL代码包含两个部分:在主机上运行的代码以及在一个或多个设备上运行的代码。主机代码可以用一般的C或C++来编写,用来创建管理“主机-设备”通信的数据结构。主机选择相应的函数(被称为内核),放到命令队列之中,然后发送到设备上。与主机代码不同,内核代码使用的是OpenCL标准中所定义的高性能功能。

在这些新的数据结构和函数操作的闪耀之下,OpenCL会显得亲和力不够。但当你自己动手开始编写程序之后,就会发现它和一般的C和C++程序编写并没有多大不同。一旦能够熟练掌握基于向量的并行编程,写出自己的应用程序,你就再也不会留恋单核编程的日子了。

下一章,我们将开始OpenCL编程的探索之旅,介绍编写主机应用程序所需的主要数据结构。

[1] 译者注:也就是GPU-GPU或者CPU-GPU,这种协同完成任务的计算架构。

[2] 译者注:对这句话以及后文的“一次编写,各设备上运行的理解”的比较,其实想讨论的是language-neutral(语言无关), platform-neutral(平台无关)的问题。因为OpenCL程序比较的底层,所以只是language-neutral层面上的。拿OpenCL和JVM做对比是有意义的,两者所起的作用都是隔开高级语言和汇编语言的直接对话。

[3] 译者注:ISA(指令集体系架构)是计算机体系架构设计的有机组成,二者之间的紧密关系决定,即使是完成的功能相同,但指令集可能没有共同之处。

[4] 译者注:作者并没有在Concurrency(并发)和Parallelism(并行)两个概念之间的区别上做过多的解释,而两者之间的区别也看起来比较隐晦,归结起来,并发就是两个任务交替执行,但不一定是同一时间点上同时执行(例如单核机器上的多任务处理),而并行就是多任务同时进行(例如多核处理器上的多任务处理),前者是操作系统层面上的,而后者是体系结构层面上的。


相关图书

GPU编程实战(基于Python和CUDA)
GPU编程实战(基于Python和CUDA)
C++并发编程实战(第2版)
C++并发编程实战(第2版)
Scala并发编程 第2版
Scala并发编程 第2版
Android 并发开发
Android 并发开发
C++ AMP:用Visual C++加速大规模并行计算
C++ AMP:用Visual C++加速大规模并行计算
Intel Xeon Phi协处理器高性能编程指南
Intel Xeon Phi协处理器高性能编程指南

相关文章

相关课程