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
  • 7.1.1 栅栏
  • 7.1.2 原子操作

Was this helpful?

  1. 第7章 OpenCL设备端内存模型

7.1 同步和交互

介绍主机端内存模型时,我们提到过,当内核在对内存进行修改时,不能保证主机端可见的数据的状态。同样,当一个工作项对地址中的数据进行修改时,其他工作项可见的数据状态是不确定的。不过,OpenCL C(结合内存模型)提供一些同步操作以保证内存的一致性,例如:执行栅栏、内存栅栏和原子操作。层级的一致性描述如下:

  • 工作项内部,内存操作的顺序可预测:对于同一地址的两次读写将会被硬件或编译器重新排序。特别是对于图像对象的访问,即使被同一工作项操作,同步时也需要遵循“读后写”的顺序。

  • 原子操作、内存栅栏或执行栅栏操作能保证同一工作组中的两个工作项,看到的数据一致。

  • 原子操作、内存栅栏或执行栅栏操作能保证工作组的数据一致。不同工作组的工作项无法使用栅栏进行同步。

7.1.1 栅栏

工作组中,编程者需要使用栅栏对工作组中的所有工作项进行同步,要使用的内置函数为work_group_barrier()。其有两个重载版本:

void
work_group_barrier(
  cl_mem_fence_flags flags)

void
work_group_barrier(
  cl_mme_fence_flags flags,
  memory_scope scope)

栅栏要求工作组中的所有工作项都要达到指定位置,才能继续下面的工作。这样的操作能保证工作组内的数据保持一致(比如:将全局内存上的一个子集数据传输到局部内存中)。其中flags用来指定需要使用栅栏来同步的内存类型。其有三个选项:CLK_LOCAL_MEM_FENCE、CLK_GLOBAL_MEM_FENCE和CLK_IMAGE_MEM_FENCE,这三个选项分别对应能被整个工作组访问到的三种不同内存类型:局部内存、全局内存和图像内存。

第二版work_group_barrier()也可以指定内存范围。其结合flags可以进行更加细粒度的数据管理。scope有两个有效参数:memory_scope_work_group和memory_scope_device。当将memory_scope_work_group和CLK_GLOBAL_MEM_FENCE一起使用时,栅栏则能保证所有工作组中每个工作项在到达同步点时,可以看到其他所有工作项完成的数据。当将memory_scope_device和CLK_GLOBAL_MEM_FENCE一起使用时,栅栏则能保证内存可被整个设备进行访问。CLK_LOCAL_MEM_FENCE只能和memory_scope_work_group一起使用,其只能保证工作组内的数据一致,无法对该工作组之外的工作项做任何保证。

7.1.2 原子操作

基于C/C++11标准,OpenCL 2.0也更新了原子操作。并且新加入的操作,不仅可以进行原子操作,还可以用来做同步。原子性能保证一系列内存操作(比如:读改写),且不需要其他工作项和主机的参与,就能直接修改某个内存的数据。当原子操作用来做同步,那么就需要对特定的变量进行访问(称为同步变量),这个变量就属于内存一致性模型的执行部分。原子操作也有多种方式,包括原子“读改写”,原子加载和原子存储。

我们之前提到过,原子操作可以保证内存的某些不一致状态不对其他线程可见——不过,这给共享内存和并发编程就是带来了一些问题。试想,当有两个线程尝试对同一个变量进行加法操作。线程0需要读取内存中的数据,然后对数值进行加法操作,最后写回原始内存中。线程1执行加法计算的过程也是一样的。图7.2就展示了同样是两个线程对同一变量进行加法操作,最后会得到不同的结果。这中问题就称为数据竞争(data race)。即使在单核机器上,也会有数据竞争的存在,比如线程0打断或抢占了正在执行操作的时间片。

图7.2 对同一变量的进行加法操作,所导致的数据竞争。最终数据的结果依赖于不同线程执行的顺序。

因此,就需要原子加载和原子存储操作来为数据竞争做一决断。C/C++11标准与OpenCL标准很像,不会保证任何加载和存储操作是绝对原子的。试想这样一种情形,将存储64位的操作分成两个指令执行。当第一个指令完成时,第二个指令还没有执行,在某些情况下是没有问题的。如果这时出现了另外一个线程,该线程执行一个加载操作,如果第二个存储指令还未完成,那么该线程所读取到高或第32位就不是最新的值。这样的读取方式显然是荒谬的,并且会得到与期望不符的结果。实际上,大多数结构中都会提供不同粒度的原子操作,来保证加载和存储数据的一致性(通常需要内存对齐在同一缓存行上)。不过,对于可移植代码来说,共享内存上的任何操作都不能认为是原子的。

原子操作在OpenCL 2.0标准做了相当多的修改。OpenCL C语言定义了与基本类型相关的原子类型,其支持整型和单精度浮点类型:

atomic_int
atomic_uint
atomic_float [1]

如果设备支持64bit原子扩展,那么就需要添加一些原子类型:

atomic_long
atomic_ulong
atomic_double
atomic_size_t
atomic_intptr_t
atomic_uintptr_t
atomic_ptrdiff_t

64位原子指针类型只针对能够使用64位地址空间的计算设备。

OpenCL C语言定义了很多的原子操作。浮点操作只支持“比较后交换”类型的原子操作(比如,atomic_exchange())。算法中的一些操作,以及一些位运算中,需要调用“同步后修改”原子函数。其声明类型如下所示:

C atomic_fetch_<key>(volatile A *object, M operand)

其中key可以替换成add, sub, or, xor, and, min和max。object传入的是原子类型的指针,operand传入的是要进行操作的数值。返回值C是非原子版的A,其值是在对M操作之前A内存中的具体数值。这里来举个原子操作的例子,当要在一个共享项中比较最小值时,我们先定义一个最小值atomic_int curMin,和一个新的值int myMin。那么就可以写成如下形式:

int oldMin = atomic_fetch_min(&curMin, myMin);

执行完成后,新的最小值将存在curMin中。是否接收返回值是可选的,不接收返回值会有潜在的性能提升。一些GPU架构中,例如:原子操作执行在内存系统中的硬件单元上。因此,在这种分层内存上原子操作很快就能执行完成。不过,如果需要使用返回值的话,通常需要将原始值从内存中读取出来,这就需要增加成千上万个时钟延迟。

内存模型的章节中对原子操作进行讨论,是因为其能进行内存同步,保证内存的一致性。不管原子操作什么时候执行,编程者都有能力指定该原子操作是否附带有同步功能,作为获取操作或释放操作。使用这种方式允许工作项能够控制可见数据访问,这样就能做到工作项间的通讯,而2.0之前的OpenCL标准是无法完成这项操作的。

[1] atomic_float 类型只支“比较后交换”类型的原子操作,不支持“同步后修改”类型操作(详见7.7.1节)

Previous第7章 OpenCL设备端内存模型Next7.2 全局内存

Last updated 6 years ago

Was this helpful?