找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 27|回复: 2

DAY48:阅读Arithmetic Functions和Bitwise Functions

[复制链接]
发表于 2018-7-10 13:14:28 | 显示全部楼层 |阅读模式
B.12.1. Arithmetic Functions

B.12.1.1. atomicAdd()
  1. int atomicAdd(int* address, int val);
  2. unsigned int atomicAdd(unsigned int* address,
  3.                        unsigned int val);
  4. unsigned long long int atomicAdd(unsigned long long int* address,
  5.                                  unsigned long long int val);
  6. float atomicAdd(float* address, float val);
  7. double atomicAdd(double* address, double val);
复制代码

reads the 32-bit or 64-bit word old located at the address address in global or shared memory, computes (old + val), and stores the result back to memory at the same address. These three operations are performed in one atomic transaction. The function returns old.
The 32-bit floating-point version of atomicAdd() is only supported by devices of compute capability 2.x and higher.
The 64-bit floating-point version of atomicAdd() is only supported by devices of compute capability 6.x and higher.



B.12.1.2. atomicSub()
  1. int atomicSub(int* address, int val);
  2. unsigned int atomicSub(unsigned int* address,
  3.                        unsigned int val);
复制代码

reads the 32-bit word old located at the address address in global or shared memory, computes (old - val), and stores the result back to memory at the same address. These three operations are performed in one atomic transaction. The function returns old.



B.12.1.3. atomicExch()
  1. int atomicExch(int* address, int val);
  2. unsigned int atomicExch(unsigned int* address,
  3.                         unsigned int val);
  4. unsigned long long int atomicExch(unsigned long long int* address,
  5.                                   unsigned long long int val);
  6. float atomicExch(float* address, float val);
复制代码

reads the 32-bit or 64-bit word old located at the address address in global or shared memory and stores val back to memory at the same address. These two operations are performed in one atomic transaction. The function returns old.



B.12.1.4. atomicMin()
  1. int atomicMin(int* address, int val);
  2. unsigned int atomicMin(unsigned int* address,
  3.                        unsigned int val);
  4. unsigned long long int atomicMin(unsigned long long int* address,
  5.                                  unsigned long long int val);
复制代码

reads the 32-bit or 64-bit word old located at the address address in global or shared memory, computes the minimum of old and val, and stores the result back to memory at the same address. These three operations are performed in one atomic transaction. The function returns old.
The 64-bit version of atomicMin() is only supported by devices of compute capability 3.5 and higher.



B.12.1.5. atomicMax()
  1. int atomicMax(int* address, int val);
  2. unsigned int atomicMax(unsigned int* address,
  3.                        unsigned int val);
  4. unsigned long long int atomicMax(unsigned long long int* address,
  5.                                  unsigned long long int val);
复制代码

reads the 32-bit or 64-bit word old located at the address address in global or shared memory, computes the maximum of old and val, and stores the result back to memory at the same address. These three operations are performed in one atomic transaction. The function returns old.
The 64-bit version of atomicMax() is only supported by devices of compute capability 3.5 and higher.



B.12.1.6. atomicInc()
  1. unsigned int atomicInc(unsigned int* address,
  2.                        unsigned int val);
复制代码

reads the 32-bit word old located at the address address in global or shared memory, computes ((old >= val) ? 0 : (old+1)), and stores the result back to memory at the same address. These three operations are performed in one atomic transaction. The function returns old.



B.12.1.7. atomicDec()
  1. unsigned int atomicDec(unsigned int* address,
  2.                        unsigned int val);
复制代码

reads the 32-bit word old located at the address address in global or shared memory, computes (((old == 0) | (old > val)) ? val : (old-1) ), and stores the result back to memory at the same address. These three operations are performed in one atomic transaction. The function returns old.



B.12.1.8. atomicCAS()
  1. int atomicCAS(int* address, int compare, int val);
  2. unsigned int atomicCAS(unsigned int* address,
  3.                        unsigned int compare,
  4.                        unsigned int val);
  5. unsigned long long int atomicCAS(unsigned long long int* address,
  6.                                  unsigned long long int compare,
  7.                                  unsigned long long int val);
复制代码

reads the 32-bit or 64-bit word old located at the address address in global or shared memory, computes (old == compare ? val : old) , and stores the result back to memory at the same address. These three operations are performed in one atomic transaction. The function returns old (Compare And Swap).




B.12.2. Bitwise Functions

B.12.2.1. atomicAnd()
  1. int atomicAnd(int* address, int val);
  2. unsigned int atomicAnd(unsigned int* address,
  3.                        unsigned int val);
  4. unsigned long long int atomicAnd(unsigned long long int* address,
  5.                                  unsigned long long int val);
复制代码

reads the 32-bit or 64-bit word old located at the address address in global or shared memory, computes (old & val), and stores the result back to memory at the same address. These three operations are performed in one atomic transaction. The function returns old.
The 64-bit version of atomicAnd() is only supported by devices of compute capability 3.5 and higher.



B.12.2.2. atomicOr()
  1. int atomicOr(int* address, int val);
  2. unsigned int atomicOr(unsigned int* address,
  3.                       unsigned int val);
  4. unsigned long long int atomicOr(unsigned long long int* address,
  5.                                 unsigned long long int val);
复制代码

reads the 32-bit or 64-bit word old located at the address address in global or shared memory, computes (old | val), and stores the result back to memory at the same address. These three operations are performed in one atomic transaction. The function returns old.
The 64-bit version of atomicOr() is only supported by devices of compute capability 3.5 and higher.



B.12.2.3. atomicXor()
  1. int atomicXor(int* address, int val);
  2. unsigned int atomicXor(unsigned int* address,
  3.                        unsigned int val);
  4. unsigned long long int atomicXor(unsigned long long int* address,
  5.                                  unsigned long long int val);
复制代码

reads the 32-bit or 64-bit word old located at the address address in global or shared memory, computes (old ^ val), and stores the result back to memory at the same address. These three operations are performed in one atomic transaction. The function returns old.
The 64-bit version of atomicXor() is only supported by devices of compute capability 3.5 and higher.





回复

使用道具 举报

 楼主| 发表于 2018-7-10 16:27:28 | 显示全部楼层
今天具体讲解各个函数的原型的.这些具体的原子操作函数都很重要.
除非在计算能力3.5以下, 在shared memory上的原子操作将被展开成一个指令序列外, 其他的原子操作都是硬件直接支持的(intrinsic functions),好在现在都是至少Maxwell+(5.0)的卡了, 这些原子操作函数都将被编译为具体的直接硬件指令(例如你在使用cuobjdump的时候看到的atom和red开头的指令(后者你可以理解成不要返回的旧值的原子操作版本)).
具体的信息也在CUDA安装目录的cuda binary utilities的pdf文档里有描述.
我们都知道, 在并行的环境中, 对共享的资源的访问很多时候是无法避免的,此时必须需要某种方式来提供一定的必要的互斥访问特性.这个时候, 你需要考虑本章节的所说的这些基本的原子操作函数, 甚至有的时候必要的情况下,你还需要使用互斥锁之类的东西, 后者有一个明显的锁定---处理的一行或者多行代码----解锁的过程.这两个方式各有利弊. 基本的原子操作, 不能提供很复杂的逻辑处理过程, 也不能嵌套上锁(例如多层锁或者多个锁)。但好处是简单, 同时不需要手工的解锁之类的过程, 同时GPU硬件允许大量的原子操作函数并行, 并通过一种或者多种方式, 来自动提供较高的吞吐率, 没有冲突的时候性能非常不错.后者则可以支持复杂的逻辑, 可以中间处理一段较长的需要被保证事务性的代码, 但坏处是很容易失手, 导致各种死锁或者活锁(活锁等于没有卡死的一处或者多处逻辑, 始终在进展中, 但永远出不来, 你可以理解成在状态反复变化的死锁), 同时大部分的实现可能性能很糟糕.
不过本手册依然在后面提供了一个mutex(互斥锁)的实现.用户可以参考.虽然我们并不推荐使用它(已经在论坛上为各个客户debug了N次它们的锁的实现了....)。
此外, 计算能力7.0+的卡, 还提供了线程休眠支持, 例如在频繁的多个线程竞争1个互斥锁的所有权的时候,可以主动避让一段时间, 提高性能(和降低功耗).所以用还是可以用的, 只是需要小心. 能用基本的原子操作, 进行lock-free的操作的时候, 还是应当只用原子操作的.除非你的代码在往GPU上移植的时候, 实在处理不了(对竞争性访问的资源的访问控制)的时候, 才应当考虑.此外, 注意本章节的函数都具有一定的操作能力要求, 当你发现你编译某段代码失败地时候, 请注意你的计算能力设定,前几天刚刚有客户说它不能在P40上使用double的原子操作加法(双精度atomicAdd),后来我们发现是它忘记设定计算能力了. 当然P40的double性能则是另外一回事.
然后请注意本章节的所有原子操作函数, 如果需要使用返回值,则返回的是旧值, 也就是说原子操作是这样地流程:
原子操作内部的不能被打断的一体流程:
(1)读取你指定的地址上的旧值
(2)进行你要求地计算(例如+1)
(3)将你指定的地址上保存计算得到的新值
(4)可选的, 返回最初的旧值.

很多客户以为这个第四4是返回的最终结果, 然而它不是的,它返回的是操作之前的老值.这点一定要注意.每年都有大量的人询问为何原子操作的结果不对了.


回复 支持 反对

使用道具 举报

 楼主| 发表于 2018-7-10 16:42:48 | 显示全部楼层
sisiy 发表于 2018-7-10 16:27
今天具体讲解各个函数的原型的.这些具体的原子操作函数都很重要.
除非在计算能力3.5以下, 在shared memory ...

此外:需要补充一下的是,如果客户需要自己实现锁, 高层的各种锁, 需要使用内置的这些基本原子操作函数来实现.常见的可以使用:
atomicCAS, 比较交换. 可以进行test-set风格的操作.这点对于CS行业的人来说, 在<操作系统>之类的课程里面, 是基本常识. 非CS行业的人需要稍微自行搜索一下资料, 看下为何可以这样来实现高级的逻辑上的锁(很容易理解. 但这里不讲解).
其次, 则是可以考虑使用atomicExch, 这个也其实比较容易理解.这两个基本的原子操作是实现很多锁的基本.应当看一下的.此外, atomicAdd也可以用来实现. 但是比用这两个更加复杂一点.用户可以自行思考. 也可以直接抄袭手册和网上的使用前两者的代码.
第二点需要说明地是, 很多资料(例如来自arxiv的一些资料), 建议在密集对同一地址进行atomicAdd之类地操作地时候(例如说, 你需要维护一个计数器),进行手工选出warp内部的1个线程, 进行1次原子操作, 然后将结果广播给32个线程,然后32个线程内部分别将旧值增加1倍,2倍, 3倍....来模拟直接进行了32次密集的原子操作, 从而能提高性能(例如, 你在global memory上的某一个地址上这样做, 而global memory上的原子操作是通过L2 cache进行的, 你可以减少一下L2 cache的劳累, 这样),但是现在的版本的CUDA编译器, 在为GPU生成代码地时候,如果它发现warp能在编译时刻确定是对同一个地址进行操作,则会自动进行warp内部选举, 自动进行上述过程, 例如将返回地值通过warp shuffle传播给各个线程, 然后自动完成这些操作.所以不需要进行这个技巧了. 这个技巧曾经也在GTC上说过. 但现在无用了(CUDA 8.0/9.2上都会自动实现).
第三点需要注意是, 还有一种另外的风格的锁, 比较弱,叫做load-lock + conditional write,也叫load-link + conditional write,可以在计算能力5.0之前的卡上的shared memory中的原子操作, 以及, 一些arm处理器上看到.这个操作本身也是像CAS/Exch/Add这三个一样, 用来实现高层的锁的.用户需要注意一点这个.(详细信息请自行搜索,这里不是操作系统现场教学)

回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

站长推荐上一条 /1 下一条

快速回复 返回顶部 返回列表