OpenCL 2.0 异构计算 [第三版] (中文)
  • Introduction
  • 序言
  • 第1章 简介异构计算
    • 1.1 关于异构计算
    • 1.2 本书目的
    • 1.3 并行思想
    • 1.4 并发和并行编程模型
    • 1.5 线程和共享内存
    • 1.6 消息通讯机制
    • 1.7 并行性的粒度
    • 1.8 使用OpenCL进行异构计算
    • 1.9 本书结构
  • 第2章 设备架构
    • 2.1 介绍
    • 2.2 硬件的权衡
    • 2.3 架构设计空间
    • 2.4 本章总结
  • 第3章 介绍OpenCL
    • 3.1 简介OpenCL
    • 3.2 OpenCL平台模型
    • 3.3 OpenCL执行模型
    • 3.4 内核和OpenCL编程模型
    • 3.5 OpenCL内存模型
    • 3.6 OpenCL运行时(例子)
    • 3.7 OpenCL C++ Wapper向量加法
    • 3.8 CUDA编程者使用OpenCL的注意事项
  • 第4章 OpenCL案例
    • 4.1 OpenCL实例
    • 4.2 直方图
    • 4.3 图像旋转
    • 4.4 图像卷积
    • 4.5 生产者-消费者
    • 4.6 基本功能函数
    • 4.7 本章总结
  • 第5章 OpenCL运行时和并发模型
    • 5.1 命令和排队模型
    • 5.2 多命令队列
    • 5.3 内核执行域:工作项、工作组和NDRange
    • 5.4 原生和内置内核
    • 5.5 设备端排队
    • 5.6 本章总结
  • 第6章 OpenCL主机端内存模型
    • 6.1 内存对象
    • 6.2 内存管理
    • 6.3 共享虚拟内存
    • 6.4 本章总结
  • 第7章 OpenCL设备端内存模型
    • 7.1 同步和交互
    • 7.2 全局内存
    • 7.3 常量内存
    • 7.4 局部内存
    • 7.5 私有内存
    • 7.6 统一地址空间
    • 7.7 内存序
    • 7.8 本章总结
  • 第8章 异构系统下解析OpenCL
    • 8.1 AMD FX-8350 CPU
    • 8.2 AMD RADEON R9 290X CPU
    • 8.3 OpenCL内存性能的考量
    • 8.4 本章总结
  • 第9章 案例分析:图像聚类
    • 9.1 图像聚类简介
    • 9.2 直方图的特性——CPU实现
    • 9.3 OpenCL实现
    • 9.4 性能分析
    • 9.5 本章总结
  • 第10章 OpenCL的分析和调试
    • 10.1 设置本章的原因
    • 10.2 使用事件分析OpenCL代码
    • 10.3 AMD CodeXL
    • 10.4 如何使用AMD CodeXL
    • 10.5 使用CodeXL分析内核
    • 10.6 使用CodeXL调试OpenCL内核
    • 10.7 使用printf调试
    • 10.8 本章总结
  • 第11章 高级语言映射到OpenCL2.0 —— 从编译器作者的角度
    • 11.1 简要介绍现状
    • 11.2 简单介绍C++ AMP
    • 11.3 编译器的目标 —— OpenCL 2.0
    • 11.4 C++ AMP与OpenCL对比
    • 11.5 C++ AMP的编译流
    • 11.6 编译之后的C++ AMP代码
    • 11.7 OpenCL 2.0提出共享虚拟内存的原因
    • 11.8 编译器怎样支持C++ AMP的线程块划分
    • 11.9 地址空间的推断
    • 11.10 优化数据搬运
    • 11.11 完整例子:二项式
    • 11.12 初步结果
    • 11.13 本章总结
  • 第12章 WebCL:使用OpenCL加速Web应用
    • 12.1 介绍WebCL
    • 12.2 如何使用WebCL编程
    • 12.3 同步机制
    • 12.4 WebCL的交互性
    • 12.5 应用实例
    • 12.6 增强安全性
    • 12.7 服务器端使用WebCL
    • 12.8 WebCL的状态和特性
  • 第13章 其他高级语言中OpenCL的使用
    • 13.1 本章简介
    • 13.2 越过C和C++
    • 13.3 Haskell中使用OpenCL
    • 13.4 本章总结
Powered by GitBook
On this page

Was this helpful?

  1. 第11章 高级语言映射到OpenCL2.0 —— 从编译器作者的角度

11.4 C++ AMP与OpenCL对比

为了将OpenCL映射到一个新的编程模型中,先将一些重要的步骤进行映射。表11.1中就展示了OpenCL相关步骤与C++ AMP的对应关系。

表11.1 OpenCL的相关步骤在C++ AMP的对应情况

OpenCL

C++ AMP

内核

parallel_for_each中定义Lambda函数,或将一个函数直接指定给parallel_for_each

内核名

使用C++函数操作符直接调用给定函数或Lambda函数

内核启动

parallel_for_each

内核参数

使用Lambda函数获取变量

cl_mem内存

concurrency::array_view或array

图11.1中使用了Lambda函数作为并行执行的具体实现。这就类似于OpenCL中的内核函数,其会并行执行在各个工作项上。

通常会使用内核名称生成具体的OpenCL内核对象,这里可以直接使用c++的函数操作符直接运行Lambda函数或给定的函数。C++中为了避免不命名冲突,使用了作用域规则生成对应的内核对象。

其余的映射就是有关于主机端和设备端交互的。C++ AMP使用parallel_for_each来替代OpenCL中使用API进行传递内核函数和启动内核函数的过程。图11.1中使用的Lambda函数可以自动获取相关的参数,也就是自动的完成了向内核传递参数这一步骤。另外,Lambda中所是用到的array_view,都可以认为是显式的cl_mem内存。

从概念上了解了二者的映射关系后,对于C++ AMP编译器来说只需要提供如下支持就可以:

  1. 如何从周围的代码中获取OpenCL内核所需要的参数。

  2. 主机端需要在一条语句中完成创建内核、准备参数和内存,以及加载内核并运行的操作。

C++Lambda函数可以当做为匿名函数,为了更加贴近于C++我们会重写这个Lambda表达式(如图11.3所示)。

图11.3 仿函数版本的C++ AMP向量相加。

这段代码中,显式的将Lambda函数携程一个仿函数版本,其可以获取变量,va、vb和vc作为这个类的输出,并且将Lambda作为函数操作符的函数体。最后,构造函数通过主机端传入的参数生成对应的输出。

不过,我们含有一些东西漏掉了:

  1. 函数操作符在这里与OpenCL内核相对应。C++ AMP中使用parallel_for_each执行对应内核函数。不过,该仿函数是一个类,需要我们先创建一个实例。

  2. 运行时如何对内核的名称进行推断?

  3. array_view在主机端可能包含有cl_mem,不过在OpenCL设备端只能操作原始cl_mem指针,而不允许在主机端对cl_mem指针进行操作。这种关系需要理清楚,以便满足主机端和设备端不同的操作。

为了弥合这些漏洞,我们需要在图11.3中的类中添加更多的东西。图11.4中的第1、21、29和31行。

图11.4 扩展之后的向量相加——C++ AMP版本

图11.4中的版本中,将三个遗留的问题进行弥补。第一行简单的定义了一个concurrency::array_view,这个简单定义并不表示其就是标准concurrency::array_view的实现。这里使用的方式就是使用宏的方式,使得主机端和设备端所在同种容器中,使用不同的宏定义情况下,具有不同的类型数据成员。注意这里我们将array_view看作为一个OpenCL内存,这里我们就需要将OpenCL内存对象放入array_view中(命名为backing_storage)。同样,我们也需要添加两个成员函数,并且需要再定义一个新的构造函数,所以有些功能需要C++ AMP编译器在编译翻译过程中进行添加:

  • 需要在编译主机端代码阶段获取内核名称

  • 其次就是需要一个OpenCL内核,并且内核代码只能在设备端编译。并且运行时主机端就需要使用到这些内核的名字。编译器需要将这些函数对象拷贝一份,并连同内核参数传入设备端。

  • 第22行的新构造函数会编译成只有设备端能使用的代码。其目的是使用新的构造函数,在GPU上够造出一个Lambda函数的副本(携带参数),参数通常使用的地址并不是外部使用的那些。这就给了编译器相对自由的空间,并且可以在这之后进行一些优化。

不过,11.4图中的代码主要描绘了CLamp如何获取每个函数的名称(调用函数操作符),新的构造函数会将Lambda表达式构造成适合于GPU端使用的函数,并且使用宏的方式让同一个数组容器可以包含主机端或设备端的内存。最后,输出的OpenCL代码中,我们将数组容器中的cl_mem对象通过clSetKernelArg()API以一定的顺序设置入OpenCL内核当中。为了满足这些要求,需要实现的编译器具有如下的功能:

  • 从主机端的Lambda函数中获取cl_mem对象,并通过OpenCLclSetKernelArg() API对内存对象进行设置。

  • 将对应的内核参数地址进行查询,并且调用新构造函数实例化与Lambda不太相同的设备端内核,以及内核参数。

  • 参数的获取应该不会受到影响,并且参数的传递过程是不透明的。例如,array_view中_sz的值就需要通过主机端传递给设备端。

为了系统的实现这些功能,需要清晰的指明,Lambda函数中需要哪种类型的数据。表11.2中描述了图11.4中哪些数据需要在设备端和主机端使用。

表11.2 将主机端的数据成员与设备端进行映射

数据成员

主机端

设备端

注意

array_view va

cl_mem va._backing_storage

__global float *va_backing_storage

通过clSetKernelArg进行传递

va的尺寸

size_t va._sz

size_t va._sz

字面方式传递

array_view vb

cl_mem vb._backing_storage

__global float *vb._backing_storage

通过clSetKernelArg进行传递

vb的尺寸

size_t vb._sz

size_t vb._sz

字面方式传递

array_view vc

cl_mem vc._backing_storage

__global float *vc._backing_storage

通过clSetKernelArg进行传递

vc的尺寸

size_t vc._sz

size_t vc._sz

字面方式传递

根据对应关系,可以通过C++ AMP的parallel_for_each生成OpenCL内核。这可以通过图11.4中的C++模板进行实现。基于表中的对应关系,可以实现对应的parallel_for_each,如图11.5所示。

图11.5 主机端的parallel_for_each实现(概念代码)

为了产生适合图11.5中的OpenCL内核点,我们需要将所要执行的对象进行遍历,并筛选出相关的对象供内核函数调用(第6-11行)。另外,生成的内核代码的参数顺序也要和主机端实现对应起来。

之前,仿函数是一种向C++ AMP传递数据的方式,不过对于OpenCL内核来说,需要将相关的数据与内核参数顺序进行对应。基本上CPU端的地址,都会要拷贝并转化成GPU端能使用的地址:函数中有太多数据成员需要拷贝,这样的拷贝最好放在初始化的时候去做。

我们之前通过值的方式进行OpenCL内存的传递。不过,复杂的地方在于我们如何将内存通过不透明的方式传递给对应句柄,需要依赖于OpenCL运行时创建对应的GPU内存空间,并将主机端的内存拷贝到GPU上。

需要注意的是,其他语言中(比如:java)序列化和反序列通过代码的顺序进行反映,这种反映并不是C++源码级别的,所以无需对编译器进行很大的改动。C++ AMP编译器中,序列化和反序列化代码的顺序,都会通过枚举数据变量的方式,进行内核参数的设置。

Previous11.3 编译器的目标 —— OpenCL 2.0Next11.5 C++ AMP的编译流

Last updated 6 years ago

Was this helpful?

这里有的读者可能会发现,图11.5中的实例代码有点类似于对象的。因为在实现的时候,我们不希望将相应的数据在外部进行存储和检索,所以我们需要压缩更多的数据,并通过clSetKernelArg()将相关数据设置到GPU内部。

序列化