CUDA优化的冷知识19|constant和寄存器

2021-02-05 14:30:09 浏览数 (1)

这一系列文章面向CUDA开发者来解读《CUDA C Best Practices Guide》 (CUDA C最佳实践指南)

大家可以访问:

https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html 来阅读原文。

这是一本很经典的手册。

CUDA优化的冷知识13 |从Global memory到Shared memory

CUDA优化的冷知识14|local memory你可能不知道的好处

CUDA优化的冷知识15|纹理存储优势(1)

CUDA优化的冷知识16|纹理存储优势(2)

CUDA优化的冷知识17|纹理存储优势(3)

CUDA优化的冷知识18| texture和surface

我们继续说一下剩下的两点小内容.

第一个是内容是constant ,这个谁都知道是什么, 也知道它的优势, 例如我们都知道GPU是RISC架构,所有的数据都要单独的load操作进入寄存器后, 才能参与运算.,但是constant例外, 很多指令允许一个操作数, 作为constant的形式存在, 从而在一条指令内部, 聚合了该指令本身的计算功能, 外加对constant的读取在一起.

这里要注意两点: (1)是7.5 的卡有单独的标量/Uniform路径, 不仅仅可以在SP的计算指令中, 集成对constant数据的读取为操作数, 从而节省了一条单独的load读取数据指令(例如常见的A = K * B C; 这里的K就可能并不需要单独一条指令载入的). 7.5 (也就是图灵 )的卡, 其标量单元, 还可以单独在SP之外, 执行标量/constant载入指令, 进一步的提供灵活性和释放向量指令(例如可以在很提前的位置进行load, 而不是在遇到了c[Bank][offset]风格的cosntant操作数的后期时刻). 但是很遗憾的是, 目前的NV家的编译器还对标量路径代码生成支持的不好. 虽然我们知道这是竞争对手A家从10年前就有的功能(的确很多方面A卡硬件好), 而且A卡的配套软件质量非常渣, 但是这点上NV的编译器质量还是不如A卡的.

好在随着以后的CUDA Toolkit版本, 驱动版本的提升必然会逐渐的效果提升的. 总之读者现在该用constant就要用. (2)点则是, 应当正确的使用constant, 这里的constant指的是手工放入__constant__中的内容. 我们在论坛常见很多楼主有很多错误/不当做法, 我举两个例子.

第一个例子是本优化实践手册这里说的, warp内的很多线程读取不同的__constant__数组中的元素, 这样做将完全失去constant的效果, 而且可能会起到反面作用(变慢).constant必须在warp一致的时候才能用. 其他使用将可能拖慢你的代码.这点论坛上已经有N个反面教材了. 另外一点是, 过度的使用__constant__, 表现为用户拼命的较近脑汁的将自己的代码中的常数, 例如1.0f, 233, 666这样的常数单独提出取出来,然后手工放入一个__constant__变量或者数组中. 从而认为这样可以"进一步的优化".其实不是的. 首先说, 编译器会自动完成这个过程, 如果它认为某些数据能够从代码中自动被提取出来, 它会自动这样做, 并放入constant.

其次, constant并不是最快的, 有些数据如果合适, 可以直接嵌入在指令的内部, 作为"立即数". 而立即数可以被保存在指令缓存(而不是任何数据缓存), 只要能取指令, 那么数据就已经免费就绪了. 所以用户手工的这样做(手工将kernel中的常数提取出来放入__constant__)是没有必要的, 甚至可能会起到反面的优化效果. 需要注意. 注意本实践手册是将其作为存储器分类的.一种是将寄存器作为存储器分类,一种是将其特化, 它就是寄存器, 而不将其作为通用意义上的存储器, 虽然也有register file(寄存器堆)之类的说法存在.

这里主要提到2个问题. 第一个问题是涉及到寄存器的bank conflict, 这点如同本优化指南说的,用户无法控制这个问题, 这个是编译器在生成目标代码的时候, 自动尽量规避的.这点我赞同. 同时本手册说了, 不用考虑用int4, float4, double2类似这种数据类型所可能带来的寄存器的bank conflict, 该用/不改用就用(不用). 这点可能是有点欲盖弥彰了.

因为在某代著名的3.5/3.7的时候(大Kepler), 压满显卡的峰值性能是如此的困难, 导致用户不得不考虑使用ILP(指令级别的线程内部的前后自我并行, 本优化指南后续章节会说). 而使用了ILP往往会导致使用int4/float4这种向量类型, 而根据已有的资料, 在大Kepler上这样做, 往往会导致严重的寄存器的bank conflict, 同时编译器竭尽全力还无法很好的避免, 这就很尴尬了. 所以手册虽然这里这样说了, 但是用户是否该用, 该如何用才是优化的, 请自行考虑.

好在现在随着时代的发展, K80这种卡已经逐渐的消失了. 再可预见的将来我们应当不太用担心这个问题了.毕竟, 如同人不能同时两次跨入同一条河流, NV总不能在同一个地方(指坑爹的Kepler架构)栽倒两次的. 这是第一点关于寄存器要说的.

第二点关于寄存器要说的则是, 很多代码, 并非使用寄存器越少越好, 也并非使用寄存器越多越好. 其寄存器的使用有个最佳点(甜点). 而这个甜点的值是无法确定的(和具体的kernel, 卡, 以及kernel和kernel间的组合情况有关). 我们前几天在老樊的群里看到有用户一本正经的讨论,我将寄存器从XXX个降低到了YYY个, 结果性能并没有提升, 为何(@*#(*@(!这个其实很正常。所以我们这里提出尽量可以考虑自动化的尝试寄存器的最佳使用点, 例如写一个脚本自动控制寄存器的用量, 用不同的用量值自动重新编译和运行评估代码, 从而能自动发现这个甜点,而不是用户自己(就像老樊的群里那样)去反复尝试, 费时费力, 可能还找不到这个最有点.

关于如何能找到这个最佳点, 我们将会在后续章节中讨论.

0 人点赞