8.4_条件代码
8.4 条件代码
硬件使用“条件代码”或CC寄存器存储通常采用的4位状态向量(符号标志、进位标志、零标志和溢出标志),这个向量用于整数比较。使用比较指令,像ISET,可以设置这些CC寄存器,并且这些寄存器可以通过断定(predication)或分支(divergence)指示执行方向。断定允许(或禁止)线程束内以线程为单位执行指令,而分支则是长指令序列的条件执行。因为SM内的处理器按照SIMD的风格执行指令,其指令执行粒度为线程束(每次32线程),分支可以用更少的代码获取结果,因为线程束内的所有线程会执行相同的代码路径。
8.4.1 断定
由于管理分支与汇聚(convergence)需要额外成本,编译器在短指令序列中使用断定技术。大多数指令的效果可以通过条件判断出来,如果条件为非真,指令即被禁用。这一禁用发生的足够早,这样经过断定的指令——像加载/存储和TEX,将会取消本应产生的内存流量。注意断定不会对全局内存加载/存储合并操作起任何不良作用。指定给线程束内加载/存储指令的地址必须引用一段连续的内存空间,即使指令进行了断定。
在依赖条件变化的指令数比较小时往往会使用断定技术。编译器使用能够支持多达约7条指令的启发试探法。除了能够避免在下文描述的管理分支同步栈的额外消耗,断定技术同样给编译器在提交微码时更多的优化机会(例如指令调度)。C语言中三元操作符(即?:)被看作是一个偏好使用断定的编译器提示。
表8-2给出了一个绝佳的断定技术的例子,这个例子使用微码表示。当在共享内存位置执行原子操作时,编译器会向共享内存位置发射代码,不断地在共享内存位置循环,直到成功地执行完原子操作。指令LDSLK(加载共享内存并加锁(load shared and lock))会返回一个条件判断代码,告诉机器是否获得了锁。随后执行操作的指令会根据这个条件代码进行分支断定。
/\*0058*/LDSLKPO,R2,[R3];/\*0060*/@POIADDR2,R2,RO;/\*0068*/@PO STSUL[R3],R2;/\*0070\*/@!PO BRA 0x58;这一代码片段同样指明断定技术和分支技术有时会协同工作。最后一条指令,如果需要的话,条件分支尝试再次得到锁,同样这是进行断定。
8.4.2 分支与汇聚
断定技术在一小段条件代码上执行得很出色,尤其是if语句的下面不存在相关联的else的时候。对于大量的条件判断代码,断定技术变得低效,因为每一条指令都要被执行,而不管指令是否真正会影响计算结果。当大量的指令导致断定的执行花销超过了断定本身带来的好处时,编译器便会使用条件分支。当线程束内代码的执行流依据条件判断而呈现几条不同的执行路径时,我们称这样的代码为分支(divergent)。
英伟达公司对于他们的硬件是如何支持分支路径的详细信息守口如瓶,并且保留在不同代次硬件上改变的权利。硬件维护一个位向量用以保存线程束中活动的线程。对于标记为不活动的线程,执行会用类似于断定技术的方式禁止。在执行分支之前,编译器执行一条特殊的指令,把这一活动线程的位压入栈。代码随后被执行两次,第一次为条件判断为真的线程执行,第二次为断定为假的线程执行。这两个阶段的执行由一个分支同步栈(branch synchronization stack)管理,Lindholm的论文对此做了详细的描述:[1]
如果线程束中的线程通过依赖于数据的条件分支而执行分支操作,那么线程束会依次执行每一个分支路径,禁用那些不在此路径上的线程。当所有的路径执行完成,线程重新汇聚到原始的执行路径。SM使用分支同步栈来管理进行分支和汇聚的独立线程。分支只发生在
一个线程束内;不同的线程束相互独立的执行,无论它们是执行共同的还是全然无关的代码路径。
PTX标准中未提到分支同步栈,所以可以看见的证明其存在的现有证据只能在cuobjdump的反汇编输出中找到。SSY指令会压入一个状态码(像程序计数器和活动线程掩码)到栈中,.S指令的前缀弹出这一状态码,并且如果任一活动线程没有按照这一分支执行,会引起这些线程执行被指令SSY压入了状态的代码路径。
SSY/.S是在线程执行发生分支时唯一需要的指令,所以如果编译器可以保证所有线程只会在某一代码路径中,你可能看不到被SSY/.S指令包括起来的分支。对CUDA中的分支来说重要的是意识到,在任何情况下,所有线程束内的线程遵循相同的执行路径是最高效的。
代码清单8-2中的循环同样包括一个很完整的既有分支又有汇聚的例子。SSY指令(0x40偏移处)和NOP.S指令(0x78偏移处)分别实现了一次分支和汇聚的操作。代码在LDSLK和后续断定指令上循环,活动线程直到编译器知道所有的线程会汇聚后才退出,并且分支同步栈可以使用NOP.S指令弹出状态码。
/\*0040\*/ SSY 0x80; /\*0048\*/ BAR.RED.POPC RZ,RZ; /\*0050\*/ LD R0,[R0]; /\*0058\*/ LDSLK P0,R2,[R3]; /\*0060\*/ @PO IADD R2,R2,R0; /\*0068\*/ @PO STSUL [R3],R2; /\*0070\*/ @!PO BRA 0x58; /\*0078\*/ NOP.S CC.T;8.4.3 特殊情况:最小值、最大值和绝对值
由于一些条件操作非常常见,硬件会直接支持它们。硬件支持取最小值和最大值的操作,既适用于整型,也适用于浮点型操作数,并且能够翻译为单条指令。除此之外,浮点指令还包括对源操作数的取反和取绝对值操作。
当min/max操作被使用时,编译器可以很好的检测出来,但是如果你想万无一失,在整型上调用函数min()/max(),在浮点数上调用fmin()/fmax()即可。
[1] Lindholm,Erik,John Nickolls,Stuart Oberman,and John Montrym.NVIDIA Tesla:A unified graphics and computing architecture.IEEE Micro,March - April 2008,p.39-55.