据说,CPU条件跳转指令在预测失败情况下的损失是大约5个时钟周期。不知道在GPU上怎样?
如果一个小循环体,指令数很少,是不是这种情况下是不是必须要展开才能提高效率?
对跳转开销有个大致的估计,好评估自己程序是否需要展开,以及如何展开。
多谢!
一般认为,如果执行了跳转,GPU实际上几乎是没代价的。(以为CPU之类的几个周期的损失是因为延迟,他只能硬件同时执行1个或者2个线程,重新从新的地址取指令然后执行,需要浪费掉一些周期)。
为何GPU被认为几乎是没有代价的,因为这个线程(实际上是warp)如果需要等待从新地址取指令,它会被其他就绪可以执行的warp给切换掉,从而掩盖了延迟。你也可以理解为无视了延迟。
神马?今日说可以基本无视延迟,为何小循环要展开呢??手册在误导你吗?
不,手册说的是另外一个方面,虽然没有延迟(例如你说的几个时钟周期的损失), 但却有指令上的:
考虑如下循环, 如果不展开:
for (int i = 0; i < n; i++ )
{
songyong2(i); //其中songyong2是一个很小的循环体的示意。
}
那么每次循环需要至少2条指令:ISETP(判断是否需要结束循环), @P BRA(跳转),
那么此时如果你的循环体编译出只有3条指令,那么实际上你一次循环需要5条,你这个循环一共需要5n条指令。
但如果完全展开,(假设n提前可知), 那么只需要3n条, 通过循环展开得到了(5n - 3n) / 3n = 66%的效率提升!!
所以你得到了:不怕像CPU那样的跳转损失周期,因为我们可以掩盖。只怕我们引入多余的指令。所以展开还是有用的。
感谢您莅临CUDAZone China!
祝您设计愉快!
引入多余的判断比较指令,这个领会了。非常感谢!
跳转的延迟应该很小吧?10个周期之类我想就可以认为没有代价了。如果像全局内存400-800个周期那就不可忽略了。对这个量级的延迟我确实有些怕怕。
不知道具体数据。
但无外乎是重新设置PC指针,重新从新地址取指令,等指令取回来,解码发射执行神马的。这些都是内部的,自然你可以无视各个流程引入的延迟(而不是外部访存那么大)。加上任何中间的小延迟(例如让I-Cache取指令,到取到), 都会切换其他就绪状态的warp执行。
所以,你真心不用担心的。
此外上文说道的@p bra之类的指令进行跳转可以基本无视代价,那么大致代价有多少呢?
虽然没有数据,但是我们可以从编译器的行为里面进行猜测:
考虑如下代码:
if (songyong2 > 123)
{
do_some_thing;
}
编译器可以如下编译:(示意)
(1)ISETP(进行判断), @!p BRA target(不满足条件越过), do_something (干活的), target: (结束汇聚于此)
也可以这么编译:
(1)ISETP(进行判断), @p do_thing1, @p do_thing2…(thing1,2是你的干活的部分), 结束
我们知道对于小的转移,编译器喜欢编译为@p这样的。
编译器做决定必然是根据一定的代价标准:
(1)跳转会引入一条多余指令
(2)在恶劣条件下(例如:warp数目很少的极端情况), 无法切换,此时打乱流水线不合算。
(3)其他需要考虑的因素。
我们知道在,例如说,你的干活部分只有5条指令的情况下,编译器可能喜欢用@p执行,而不是分支;而在10条的情况下,编译器选择了分支。那么我们知道,编译器认为BRA跳转的代价(上述的1,2,3),是大于等于多执行5条, 而小于多执行10条的(因为@p可能是假执行,白干).
既然连最坏白执行10条指令的代价都没有,所以代价真心不大。仅供参考。
我来补充一下2#。
根据2#的说法,之所以CPU跳转损失的数个周期会影响效能是因为通常CPU每个核只能硬件执行1~2个线程,此时执行单元就空闲了,处于等待。而GPU可以通过切换一个就绪的thread warp来保证执行单元继续处于工作状态。实际上此种切换无处不在,GPU就是依靠此种切换来掩盖各种延迟的。如果是比较短的延迟,比如说一二十个周期的延迟,可以很容易地被掩盖;如果是访存等长延迟操作,只要有足够多的线程数量,一样可以掩盖。
不过需要补充的是,如果您的分支不是按照warp对齐的,也就是说同一个warp内部的线程会有多种不同的执行路径,那么一般来说会降低性能。(除了一个例外,就是warp内部有两个分支,其中一个分支啥事不干,直接返回。这种情况问题不大。如果是多个复杂的分支,影响较大)此时推荐您修改算法或者代码实现,使得分支是按照warp对齐的。
发现横扫千军斑竹已经在5#详细解释了编译器的行为,以及可能的代价,建议参考。