DAY41:阅读Synchronization Functions

我们正带领大家开始阅读英文的《CUDA C Programming Guide》,今天是第41天,我们正在讲解CUDA C语法,希望在接下来的59天里,您可以学习到原汁原味的CUDA,同时能养成英文阅读的习惯。

本文共计369字,阅读时间10分钟

前情回顾:

DAY36:阅读”执行空间"扩展修饰符

DAY37:阅读不同存储器的修饰符

DAY38:阅读存储器修饰符

DAY39:阅读扩展数据类型

DAY40:阅读Memory Fence Functions

B.6. Synchronization Functions

void __syncthreads();

waits until all threads in the thread block have reached this point and all global and shared memory accesses made by these threads prior to __syncthreads() are visible to all threads in the block.

__syncthreads() is used to coordinate communication between the threads of the same block. When some threads within a block access the same addresses in shared or global memory, there are potential read-after-write, write-after-read, or write-after-write hazards for some of these memory accesses. These data hazards can be avoided by synchronizing threads in-between these accesses.

__syncthreads() is allowed in conditional code but only if the conditional evaluates identically across the entire thread block, otherwise the code execution is likely to hang or produce unintended side effects.

Devices of compute capability 2.x and higher support three variations of __syncthreads() described below.

int __syncthreads_count(int predicate);

is identical to __syncthreads() with the additional feature that it evaluates predicate for all threads of the block and returns the number of threads for which predicate evaluates to non-zero.

int __syncthreads_and(int predicate);

is identical to __syncthreads() with the additional feature that it evaluates predicate for all threads of the block and returns non-zero if and only if predicate evaluates to non-zero for all of them.

int __syncthreads_or(int predicate);

is identical to __syncthreads() with the additional feature that it evaluates predicate for all threads of the block and returns non-zero if and only if predicate evaluates to non-zero for any of them.

void __syncwarp(unsigned mask=0xffffffff);

will cause the executing thread to wait until all warp lanes named in mask have executed a __syncwarp() (with the same mask) before resuming execution. All non-exited threads named in mask must execute a corresponding __syncwarp() with the same mask, or the result is undefined.

Executing __syncwarp() guarantees memory ordering among threads participating in the barrier. Thus, threads within a warp that wish to communicate via memory can store to memory, execute __syncwarp(), and then safely read values stored by other threads in the warp.

Note: For .target sm_6x or below, all threads in mask must execute the same __syncwarp() in convergence, and the union of all values in mask must be equal to the active mask. Otherwise, the behavior is undefined.

本文备注/经验分享:

今天将主要说一下__syncthreads*()家族的系列函数, 以及, CUDA 9新增的__syncwarp()。

前者主要是在一个block的范围内进行控制——代码执行位置的同步, memory fence的行为(硬件或者软件上)---昨天的内容说过这个fence. 而__syncwarp()作为新增内容, 主要是CUDA 9引入为了适应新卡的, 范围则缩减为warp内部。 先说一下传统的经典的, 大家喜闻乐见的__syncthreads()家族.这个是从CUDA最初就开始有的函数, 可以完成局部同步, 也就是block内部的同步, 同时附带memory fence效果(这点请参考昨天内容).往往用于在shared memory上的数据交换操作. 但需要注意的是: (1)对于数据量很大的内容, 可能shared memory的空间不够(48KB小于计算能力7.0, 最多96KB大于等于计算能力7.0). 此时可以需要通过global memory进行交换. 但通过local memory是不可以的, 因为线程间的同一个local memory上的地址中的内容是不同地(参考之前的local memory章节).但block内部通过global memory进行数据交换的场合比较少见.

(2)对于数据量非常少的内容, 例如1-bit级的交换, 对于常见操作, 可以由__syncthreads()的扩展版本直接完成, 连shared memory都不需要: 这包含常见地计数(1-bit值的原子累加), bool的and和or操作。也就是扩展的__syncthreads_count(), __syncthreads_and(), __synchtreads_or() 这三种常见的1-bit级别的数据交换, 如果能落到这三种最常见的用途上, 可以直接由__syncthreads()完成, 不需要走shared memory, 但如果是其他特别地数据操作类型, 例如要求每4个线程进行累加一次, 那么只能走shared memory. 但好在这三种基本上是最常见的情况了。 (3)对于数据量能在1个或者多个4B或者8B的情况下, 同时交换范围能在warp内部, 可以直接考虑warp shuffle操作, 该操作具有典型的shared memory级别的延迟, 但不需要写入任何shared memory中的存储单元, 不破坏任何内容即可完成交换. 例如某卡, 可以在正好用满48KB的(或者2-3个32KB的shared memory)的时候, 还能通过warp shuffle进行类似shared memory上的数据交换(逻辑上等于使用shared memory写入, 然后立刻读取, 但下标进行了变换, 等效的完成了数据交换), 不需要破坏或者临时保存shared memory上的内容。 其中第二点的count操作(__syncthreads_count()函数), 很多时候非常有用.在最初引入了扩展版本的__syncthreads()的时候(计算能力2.0/2.1), 普通的__syncthreads(), 也就是没有任何后缀地版本, 实际上被编译成int result = __syncthreads_count(1); 然后立刻丢弃掉结果。因此实际上不用担心使用了具有这些数据交换/统计能力的扩展版本的__syncthreads()家族函数, 是否会影响性能.你可以从上面的看到, 这实际上等于总是使用了扩展版本的__syncthreads(). 因此可以放心。

什么叫丢掉结果? N卡很多这种指令生成的. 为了满足逻辑,例如有uint32_t a,b,c; a = b + c; 然后对于进位标志的判定, carry = (a < b). 如果你的代码只需要使用carry标志, 则编译器可能生成一条直接浪费掉累加结果的带有进位输出的加法,这往往涉及到RZ寄存器的使用. 还记得它吗? 我们之前说过.为何一个线程只能使用63个寄存器而不是64个.或者为何一个寄存器只能使用255个寄存器, 而不是256个,其中的一个寄存器专门用作黑洞用途。专用用来往里写入结果, 然后丢弃的。 实际上在最初, 无论是扩展版本的__syncthreads_count()还是普通版本的__syncthreads(),都会生成同样的类似BAR.RED.POPC RZ, RZ, RZ, PT这种操作的。这条指令同时完成了同步(以及别忘记memory fence效果), 外加结果统计。只是丢入了黑洞寄存器不要了而已。

这里是用来说明, 其实这些扩展版本的代价非常低,应当需要使用就使用——肯定比你手工在shared memory进行规约快,不过从Kepler开始(计算能力3.0), 请注意这是从9.0开始的最低支持版本了.(CUDA 8.0最低的支持版本是2.0. 所以请尽快扔掉你手头的C2050之类的卡)。 从这代开始到计算能力7.2(TX2的继任者), 将普通的__syncthreads()和扩展版本的__syncthreads*()分开了.前者将只生成一条BAR.SYNC操作指令.而后者则会生成两条指令. 前者的一条, 外加一条B2R.RESULT指令用来收集结果.这可能是为了降低在Barrier上的等待延迟. 也可能有其他硬件涉及上的考虑.毕竟这些扩展版本同时有2个功能.有的时候在使用了同步功能后, 不一定需要立刻使用统计/数据交换功能的结果.此时新版本的设计"可能"会有更好的性能.因此新版本也应当使用它们(当适用的时候)。 最后需要说明的是, 从CUDA 9引入的, 为了新卡设计的__syncwarp(可选mask参数在此)函数.因为之前章节中说过, SIMT架构从7.0开始, 允许部分情况下的warp不一致执行,此时__syncwarp带来了2个效果,一个是常见的memory fence效果,另外则允许短暂地warp内部部分或者全部线程执行到特定的点.例如当你需要进行warp内部的数据交换,而不想通过全局的__syncthreads()进行. 或者warp shuffle不适用的时候.需要注意的是,以前的老代码, 进行隐式warp交换(通过volatile指针 + shared memory), 可能你需要额外改写, 避免在新卡上挂掉.以及, 此函数根据实验, 在老卡上(Pascal或者更低计算能力),将被编译成空白操作。 因此虽然你现在依然在使用老卡,但依然可以安心的升级到9.2, 而不必坚持8.0之类的老版本, 因为: (1)及时的更换到新编译器有助于你熟悉新式的写法, 将来你总有一天会买新卡的. 现在熟悉有助于降低到时候地升级版本, 避免手头的老式代码写法出来的东西越来越多, 到时候集中移植会是个灾难。 (2)升级到新版本的CUDA 9.2, 不会降低你老卡的性能, 这函数使用后可以理解成编译成空白操作,或者理解成被编译器忽略. 因此总是建议更新的(你迟早的更新, 除非你打算永远不买新卡 )。 以及, 在计算能力7.0的卡上,__syncwarp()有时候会被编译器直接忽略, 有的时候在没有使用__syncwarp()函数的时候, 编译器自动插入生成了SYNCWARP指令.而有的时候, __syncwarp()被编译成一系列稍微复杂的指令序列. 但为何会这样目前尚不明确(欢迎读者补充). 此外, 需要补充的是,扩展版本的__syncthreads*()家族函数,均接受一个同样地(int predicate)风格的参数,但在实际的硬件上(2.0-7.2),硬件指令接受的是Predicate寄存器(1-bit值),很多时候你会发现编译器无辜的反复在整数表示的布尔值(例如int a = 1)和P寄存器之间来会切换.但这可能是因为传统上C没有1-bit布尔值的规范定义导致的.不一定是编译器(弱智)或者是设计上的问题.但这一般情况下不会造成性能影响. 只是这里需要说明一下.

有不明白的地方,请在本文后留言

或者在我们的技术论坛bbs.gpuworld.cn上发帖

原文发布于微信公众号 - 吉浦迅科技(gpusolution)

原文发表时间:2018-07-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大数据钻研

一个基于Java的开源URL嗅探器

这是一个可以检测并规范化文本中的URL地址的Java库。 ? 今天,我们很高兴做一个分享,因为我所在的 Linkedin 公司 开源了我们做的一个ULR探测工具...

40411
来自专栏一名合格java开发的自我修养

java使用Map做缓存你真的用对了吗?弱引用WeakHashMap了解一下

序:使用java的Map做缓存,你是否考虑过容量导致的OOM问题,是否考虑命中率对性能的影响??

2871
来自专栏C/C++基础

CVTE2017秋季校招一面回忆(C++后台岗)

2016.9.9日下午再一次参加了CVTE的C++后台开发岗的面试,面试经历了1个小时20分钟左右的时间,被问及了很多问题,很多问题也没有回答出来,自己还是存在...

2562
来自专栏owent

Rust的第二次接触-写个小服务器程序

蛮久前入门了一下 Rust 语言。它的设计模型非常地吸引C/C++的开发者。但是学习语言嘛还是要练习一下,之前也用它给我们项目写了个命令行小工具。这回拿来写个小...

1K3
来自专栏Java社区

Java核心技术讲解学习

1443
来自专栏CDA数据分析师

热度碾压 Java、C#、C++的 Python,为什么速度那么慢?

眼下 Python 异常火爆,不论是 DevOps、数据科学、Web 开发还是安全领域,都在用 Python——但是它在速度上却没有任何优势。

1421
来自专栏大史住在大前端

大前端的自动化工厂(5)—— 基于Karma+Mocha+Chai的单元测试和接口测试

大多数前端开发者对测试相关的知识是比较缺乏的,一来是开发节奏很快,来不及写,另一方面团队里也配备了“人肉测试机”,完全没必要自己来。但随着项目体量的增大,许多人...

1142
来自专栏王硕

原 PostgreSQL的FSM分析记录

831
来自专栏企鹅号快讯

程序员大神教你学C语言/C加加编程零基础新手入门

第一章:编译器和程序 很多小伙伴都老是会碰到疑问,其实还是基础没打扎实,这些题如果你不看答案你能知道多少呢?如果还有很多不知道就证明基础没打扎实,如果你还在入门...

47111
来自专栏SDNLAB

Ryu的一些设计方法解读

作为一个业余研究Ryu的软件工程师,一直惊叹于Ryu设计的优雅与简洁。一年多坚持下来,也有自己的一些收获,写出来和大家分享一下。 我们的故事从@set_ev_c...

3116

扫码关注云+社区

领取腾讯云代金券