DAY34:阅读算术指令

5.4.1. Arithmetic Instructions

Table 2 gives the throughputs of the arithmetic instructions that are natively supported in hardware for devices of various compute capabilities.

(扫描二维码看Table 2,注意扫描后要停顿一下表格才会出现哟)

Other instructions and functions are implemented on top of the native instructions. The implementation may be different for devices of different compute capabilities, and the number of native instructions after compilation may fluctuate with every compiler version. For complicated functions, there can be multiple code paths depending on input. cuobjdump can be used to inspect a particular implementation in a cubin object.

The implementation of some functions are readily available on the CUDA header files (math_functions.h, device_functions.h, ...).

In general, code compiled with -ftz=true (denormalized numbers are flushed to zero) tends to have higher performance than code compiled with -ftz=false. Similarly, code compiled with -prec div=false (less precise division) tends to have higher performance code than code compiled with -prec div=true, and code compiled with -prec-sqrt=false (less precise square root) tends to have higher performance than code compiled with -prec-sqrt=true. The nvcc user manual describes these compilation flags in more details.

Single-Precision Floating-Point Division

__fdividef(x, y) (see Intrinsic Functions) provides faster single-precision floating-point division than the division operator.

Single-Precision Floating-Point Reciprocal Square Root

To preserve IEEE-754 semantics the compiler can optimize 1.0/sqrtf() into rsqrtf() only when both reciprocal and square root are approximate, (i.e., with -prec-div=false and -prec-sqrt=false). It is therefore recommended to invoke rsqrtf() directly where desired.

Single-Precision Floating-Point Square Root

Single-precision floating-point square root is implemented as a reciprocal square root followed by a reciprocal instead of a reciprocal square root followed by a multiplication so that it gives correct results for 0 and infinity.

Sine and Cosine

sinf(x), cosf(x), tanf(x), sincosf(x), and corresponding double-precision instructions are much more expensive and even more so if the argument x is large in magnitude.

More precisely, the argument reduction code (see Mathematical Functions for implementation) comprises two code paths referred to as the fast path and the slow path, respectively.

The fast path is used for arguments sufficiently small in magnitude and essentially consists of a few multiply-add operations. The slow path is used for arguments large in magnitude and consists of lengthy computations required to achieve correct results over the entire argument range.

At present, the argument reduction code for the trigonometric functions selects the fast path for arguments whose magnitude is less than 105615.0f for the single-precision functions, and less than 2147483648.0 for the double-precision functions.

As the slow path requires more registers than the fast path, an attempt has been made to reduce register pressure in the slow path by storing some intermediate variables in local memory, which may affect performance because of local memory high latency and bandwidth (see Device Memory Accesses). At present, 28 bytes of local memory are used by single-precision functions, and 44 bytes are used by double-precision functions. However, the exact amount is subject to change.

Due to the lengthy computations and use of local memory in the slow path, the throughput of these trigonometric functions is lower by one order of magnitude when the slow path reduction is required as opposed to the fast path reduction.

Integer Arithmetic

Integer division and modulo operation are costly as they compile to up to 20 instructions. They can be replaced with bitwise operations in some cases: If n is a power of 2, (i/n) is equivalent to(i>>log2(n)) and (i%n) is equivalent to (i&(n-1)); the compiler will perform these conversions if n is literal.

__brev and __popc map to a single instruction and __brevll and __popcll to a few instructions.

__[u]mul24 are legacy intrinsic functions that no longer have any reason to be used.

Half Precision Arithmetic

In order to achieve good half precision floating-point add, multiply or multiply-add throughput it is recommended that the half2 datatype is used. Vector intrinsics (eg. __hadd2, __hsub2,__hmul2, __hfma2) can then be used to do two operations in a single instruction. Using half2 in place of two calls using half may also help performance of other intrinsics, such as warp shuffles.

The intrinsic __halves2half2 is provided to convert two half precision values to the half2 datatype.

Type Conversion

Sometimes, the compiler must insert conversion instructions, introducing additional execution cycles. This is the case for:

· Functions operating on variables of type char or short whose operands generally need to be converted to int,

· Double-precision floating-point constants (i.e., those constants defined without any type suffix) used as input to single-precision floating-point computations (as mandated by C/C++ standards).

This last case can be avoided by using single-precision floating-point constants, defined with an f suffix such as 3.141592653589793f, 1.0f, 0.5f.

本文备注/经验分享:

今天就到了计算方面了。 其实一张GPU的主要性能或者说力量, 就在于访存和计算上.不仅仅显存的带宽远远超过内存, 计算上也性能摇摇领先, 但还是有一些需要注意的地方。 table 2 这个表是原生(native)指令的吞吐率表. 请注意, 每一代的卡, 随着时代的进步和侧重点的不同, 你会发现虽然特性上所有的运算都基本能支持, 但是它们的性能在每张卡上可能会不同.(例如double, 例如half) 这样, 挑选什么样的卡, 可以参考一下此表. 例如你可以从此表看出, 6.1的Pascal(例如GTX1080), 单精度性能是128指令每SM每周期, 但是double, 却只有4指令/SM/周期,也就是double性能对于6.1的卡只有1/32,你就知道6.1的1080这种卡, 不适合双精度运算.因为它的双精度性能只有单精度性能的3%左右(1/32) 而你会发现, 6.0的卡(GP100), 却具有32 / 64 = 50%的相对峰值单精度性能的双精度性能, 因此这卡适合双精度运算(目前最高的双精度就是50%性能--基准是用的单精度性能) 类似的, 你会看到其他性能指标. 一个很典型的历史变迁就是half,随着深度学习的越来越重要, half性能总是不断在提升,此表指出, 一些老卡, 根本就不支持half(例如Kepler, 例如5.0, 5.2(这两个是非嵌入式的Maxwell),而到了5.3(这是TX1)和Pascal以后, 所有的卡都添加了half支持.这样, 深度学习应当考虑新卡. 需要这里half指出的一点是:6.1虽然是新卡, 但half只有兼容性支持.7.0虽然这里列出的是128(200%相比它的64的单精度性能), 这是只算了SP的. TensorCore还能额外贡献大约800%的half(以及部分FP32加法)性能. 类似的, 对深度学习这表没有整理出来的是, INT8(DP4A)性能, 6.1的卡应当是400%. 7.0暂时未知.所以用来推导, 6.1的Pascal还是很好的卡, 又便宜又好。 其次这个表格需要注意的是, 32-bit整数的性能. 这里有个有趣的数字, 对于Kepler, 32-bit整数加法, 和哪怕逻辑运算, 都不是满速率的(160/192 = 83%),这说明Kepler里面的192个SP纯粹是被设计成容易打酱油的(实测的基本整数操作的性能, Kepler甚至连这个还不如, 实测往往在66%左右)。 正因为Kepler里面的SP虽然数量巨多, 但是不容易发挥性能(实际上, 它的标称100%速率的float也不容易发挥出来性能的), 所以看上去一张几千个SP的Kepler卡,从现在的角度来说, 基本不值得购买(除了K80),用NV在Maxwell优化指南里面的一句话说:Maxwell(也包括Pascal)的一个128 SP的SM,基本上等于一个Kepler的192 SP的SM,两者性能差距在10%以内等等.(居然又吐槽了一次Kepler)。

此外, 这个表格实际上不用全看, 自己从事的行业需要侧重用什么,就看什么方面的性能,例如我在做扩展精度的大整数乘法, 应当考虑这里的整数乘法之类, 和整数加法之类的性能,例如我侧重挖矿, 就应当考虑32-bit逻辑运算, 移位, 类似这种的性能。Maxwell/Pacsal+针对挖矿进行了单独优化, 这里的32-bit逻辑运算写着是100%性能, 但实际上是200%性能(需要特定的情况下, 编译器能综合出来特定的指令)。Maxwell+可以直接实现D = A ^ B ^ C这种三源操作数的按位逻辑操作(精确的说, 可以实现任何3输入的组合逻辑, 在一个指令里),类似的, 大家还能看到32-bit shift的性能提升,从maxwell+,现在普通移位, 循环移位等等, 都是50%速率的了. 类似这些. 针对Maxwell/Pascal+的额外一点提示是,很多半速率的指令(例如刚才的移位或者整数比较之类的操作), 在特定的指令配比下, 可以变身成100%速率的.这是因为从Maxwell起, SP被分配成固定的4组 * 32个 = 128个,每个周期每组SP会接受同1条指令, 一旦下一条指令不和本条指令竞争具体的执行单元(例如移位单元, 下一条指令是整数加法), 那么本条指令就能等效的变成全速率的.这点实际上往往导致maxwell和Pascal具有令人惊讶的性能(比你手工计算出来的理论性能峰值还好),而AMD的GCN卡采用了类似的结构, 但是4 * 16 = 64个SP每个CU里面, 会每组SP被连续给出4条同样指令,因此不能享受到这个福利。 因此, maxwell除了双精度不好, 和没有half/INT8支持, 其他均是一张好卡.而从Pascal起, 开始补充了double, half, INT8这些短板,同时Maxwell有的特性它都有, 因此几乎开始变成了全能的卡, 无论你是科学计算, 还是深度学习, 还是挖矿.此外, 此表的需要说明的是, maxwell的32-bit整数乘法, 其实是标准的1/4速率的(和A卡一样), 并不存在任何性能短板,这个性能比此表中的前一个(Kepler)的Single Instruction的性能实际上还要好。Pascal类似(Pascal完全在这里和Maxwell一样) 虽然这里标记的是"Multiple Instructions", 但这是有其他原因.感兴趣的可以参考一下网上的关于Maxwell/Pascal的XMAD指令(16-bit * 16-bit + 32-bit -> 32-bit整数操作, 全速率),因此maxwell在纯整数操作, 例如大整数或者超过64-bit的任意精度浮点运算,均具有良好性能(Pascal完全一样), 此外, 手册说明了部分函数具有快速版本,快速版本性能高, 但是精度低.在不需要太高精度的场合, 应当考虑低精度但更快速的版本的.这些快速版本往往也叫原生版本, 或者叫intrinsic函数, 往往具有两个__开头,用户应当注意这些。例如举个例子说, 在特定范围内的正弦(单精度版本)函数,快速版本到了最后的SFU(特殊功能单元)里面的计算一次sin后就了结,而高精度版本的后面还跟随有牛顿迭代,后续的迭代是用的SP软件算的, 因为会有较低性能---但有较高的精度。

继续谈一下这章说的较慢的高精度的数学运算和较快的低精度之间的取舍问题.其实手册后面有个表, 大致是各种运算的误差情况.里面有快速版本和高精度版本的误差比较, 单位是ULP (ULP是用最低有效位做单位的, 不过这个可以看成是某种相对于值本身的相对误差, 例如一个24-bit有效位的float值, 在正常的情况下, 一个ULP误差大约相当于,这个数的值本身的1/2^24这个级别。而double的一个ULP的误差可能大约相当于这个值的本身的1/2^53左右的误差.

例如这样的. 注意NV这里的0误差应当精确的说, 是小于0.5个ULP(下一个位, 1/2了, 因为是二进制么)的误差, 不过这已经是最精确的结果了。 从这个表可以看出, 哪怕使用了原生版本的函数或者指定了不需要高精度, 实际上的结果的精度也还是非常高的.因此一般不用担心。 此外, 需要说明的是, N卡长期提供FMA,可以对d = a * b + c的中间结果,也就是(a * b) + c这一步, 根据NV的文档(其他地方能看到, 不在这本里), 说,中间结果....取....无限精度....而AMD的说法是:对d = a * b + c(都是float)的计算,中间结果....提升精度到...double精度,虽然这个对float的融合乘加操作(FMA)的文档描述不能说明什么,但是NV既然能这样写, 证明对精度也许比AMD更有信心.类似的, 实际上用户可以分别尝试本章给出的标志, 看看结果上的变化.但因为CUDA本身的并行化, 和基本从一开始就有FMA操作(CPU是这两年才开始普及的FMA),一般情况下, 很多时候GPU和CPU的结果不同, 往往是GPU的结果更加正确(和手工计算的精确结果相比),也就是, 目前N卡算的又快又好.而并非是很多人想象中的, CPU结果更正确. 类似的, 本章节还说了一些其他方面, 例如a / N (N是2的幂)可以用移位来取代.这个如果N在编译时刻可知的常数.现在的CUDA编译器会自动发现这点, 不需要手工操作了,类似的, 还提到了一些类似__popc之类的函数,这些其实都很有用, 例如在prefix-sum或者压缩一个list去掉其中的空位之类的算法的时候. 最后说一下__umul24这种,这个是以前的1.X时代提供的24-bit * 24-bit的乘法.曾经在很多老代码中可以看到能大量的使用它,当年GPU提供它是因为它可以几乎免费的实现.(可以重用float的浮点运算中, 移位对齐后面的乘法电路),Fermi和Kepler放弃了它, 改成单独实现的32-bit乘法.而Maxwell/Pascal则提供了16-bit的版本, 依然可以重用float的运算电路.也就是之前的说的XMAD,现在的硬件上, 任何整数乘法应当直接写出,编译器会自动综合的. (可惜现在直到CUDA 9, 编译器对整数乘法的综合效果依然不好,但依然可以秒杀AMD的OpenCL编译器. AMD的OpenCL编译器在这方面直接就是弱智)

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

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

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

原文发表时间:2018-06-19

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏窗户

scheme实现最基本的自然数下的运算

  教一个基本没编过什么程序的朋友scheme,为什么教scheme呢?因为他想学,因为一直听我鼓吹,而他觉得他自己多少有C语言一点基础,而又因为我觉得函数式才...

543
来自专栏大数据文摘

数学菜鸟的AI学习攻略 | 数学符号轻松入门

2264
来自专栏CDA数据分析师

如何使用R语言解决可恶的脏数据

在数据分析过程中最头疼的应该是如何应付脏数据,脏数据的存在将会对后期的建模、挖掘等工作造成严重的错误,所以必须谨慎的处理那些脏数据。 脏数据的存在形式主要有如下...

1955
来自专栏数据小魔方

ggplot2学习笔记——图例系统及其调整函数

最近确实更得太少了,也不知道自己在忙啥,反正感觉不到忙碌的收获,要不是好多小伙伴儿在后台催更,感觉都快忘了还有要更新公众号这回事儿, 进入2018年以来,1月份...

36512
来自专栏Vamei实验室

纸上谈兵: 数学归纳法, 递归, 栈

数学归纳法 数学归纳法(mathematical induction)是一种数学证明方法,常用于证明命题(命题是对某个现象的描述)在自然数范围内成立。随着现代数...

2156
来自专栏腾讯社交用户体验设计

如何让你的动画更自然-运动曲线探究与应用

993
来自专栏Renderbus云渲染农场

vray渲染速度慢的影响因素和提升技巧-Renderbus

模型因素 较为复杂的模型(特别是存在较多细小转角的模型),会耗费更多的渲染计算时间。模型的复杂程度对渲染的影响较大,这个问题可以说是“硬件伤”。

1694
来自专栏HansBug's Lab

JSOI2015 一轮省选 个人题解与小结

T1: 题目大意:现有一个以1为根节点的树,要求从1开始出发,经过下面的点然后最终要回到根节点。同时除了根节点之外各点均有一个权值(即受益,每个点上的收益只能拿...

3348
来自专栏机器之心

一文读懂遗传算法工作原理(附Python实现)

2895
来自专栏PPV课数据科学社区

一文读懂遗传算法工作原理(附Python实现)

几天前,我着手解决一个实际问题——大型超市销售问题。在使用了几个简单模型做了一些特征工程之后,我在排行榜上名列第 219 名。

1354

扫码关注云+社区