作者:Yaong
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
 
if-then-else、loop控制语句的后端实现
 
本文是通过代码而来,主要记录了在SIMD指令集上,编译器后端对控制语句(if-then-else、loop)的指令生成方法。
 
引言:
"A unique feature of most GPU’s is that they are designed to run many different instances of the same program in lock step in order to reduce the size of the scheduling hardware by sharing it between many different “cores.” When control flow diverges, meaning that when two different instances (fragments, vertices, etc.) branch in different directions, then the GPU will take both sides of the branch. For example, if both thread 1 and thread 2 are currently in block A, and thread 1 wants to branch to block B while thread 2 wants to branch to block C, then the GPU will first branch to block B with thread 1 enabled and thread 2 disabled, and then when execution reaches a predefined “merge block,” the GPU will jump back to block C while flipping the enabled threads and run until the merge block is reached, at which point the control flow has converged and both thread 1 and thread 2 can be enabled."
 
if-then-else语句
 
进入到if-then-else语句块,转换为branch指令时,因为使用的是SIMD指令,所以各个channel的跳转控制流程可能会出现分歧。
在此我们先假设condition等于true,执行if-then语句块;condition等于false,执行else语句。
当各个channel的condition相同时,各个channel不会出现分歧,各个channel的跳转地址(代码块)也是相同的,这种情况下,当condition等于true时,所有channel执行if-then语句块,反之,condition等于false,所以channel执行else语句。
当各个channel的condition条件不一样,那么各个channel就会出现分歧,if、else两个语句块都需要被按条件被执行一次。对于condition等于1的channel,需要执行if-then语句块,而不能执行else语句;对于condition等于false的channel,要执行else语句,而不能执行if-then语句块。
最后,进入到汇合点后,所有的channel又可以并行的执行相同的指令了。
 
如何处理上述的两种情况呢?
首先这需要机器指令级别上的支持,不同的处理器有不同的实现。
来看看VC4提供的指令方案:
1.每个channel有独立的标志位,比如,N、Z、C
2.运算指令的执行结果能设置每个channel的标志位
3.指令支持条件执行
4.branch指令支持跳转条件,且支持各个channel标志位间的逻辑运算
 
具体来看.
 
伪代码:
sf: set flag
zs: zero set
zc: zero clear
 
01:    mov    execute, 0
 
02:    OR.sf    tmp0, condition, 0
03:    mov.zs    execute, @else_block
04:    mov.sf    null, execute
05:    Branch.all_zero_clear    @else_block
 
06:    then_block:
07:    …
08:    mov.sf    null, execute
09:    mov.zs    execute, @after_block
10:    sub.sf  tmp1, execute, @after_block
11:    branch.all_zero_set    @after_block
12:    end_then:
 
13:    else_block:
14:    sub.sf tmp2, execute, @else_block
15:    mov.zs execute, 0
16:    …
17:    end_else:
 
18: after_block:
 
伪代码中condition表示if的条件“if(condition)”,而execute实时记录了各个channel的执行条件。
 
当各个channel的condition相等时,这种情况相对简单些,各个channel执行的流程是相同的,要么执行if-then语句块,那么执行else语句。
当各个channels的condition值不相等时,“if-then”“else”两个语句块,均要被执行一次,但是,不是每个channel都要执行,只有在进入到语句块中execute等于零的channel才能执行其中的指令,而非零值的channel不能执行语句块中的指令(非零值是等于else语句块的index号,指向执行完当前语句块后,接下来要进入执行的语句块)。具体来看,执行流程首先(PC指针)进入到“if”基本块中,对于execute值等于0的channel会执行基本块中的指令,非0的要跳过基本块中的所有指令。在if-then语句块执行后,执行过“if”语句的channel,就不能再执行else语句的指令,所以在if-then语句块结束后需要对execute更新,在执行if-then语句块时execute等于0的channel,需要把execute更新为after block index,即执行完else基本块后的基本块的index号,非0的channel的execute需要更新为0,表明在接下来的else基本块中需要执行指令,所以在执行流程(PC指针)进入else基本块中后,同样的execute等于零的channel才会执行指令,非零不执行.
 
进入某个语句块前,需要对execute进行测试,进入某个语句块前execute保存了各个channel的将要执行的语句块的地址。
对满足if语句块的channel的退出if语句块时,需要将execute更新为下一个执行的语句块地址,即@after_block。
 
回到伪代码:
 
当我们开始处理if-then-else语句时,我们先判断执行条件,以取得下一句指令的入口,这时可能出现3种情况:
1.所有channel的condition等于0,则跳转到else_block
2.所有channel的condition不等于0,则执行if_block,并且在if_block执行完毕后,要跳过else_block。
3.各个channel的condition不全为零,按标志位,先执行if_block,再执行else_block。
 
1.将execute初始化为0,execute保存了各个channel的执行条件,在进入if-then语句块后,对execute等于0的channel,要执行if-then语句块中的指令,在进入else语句后,这些channel就不能再执行其中的else语句中的指令了。对不执行if-then语句块的channel,对应的execute值等于@else_block。
2.将condition与0做比较,并且运算结果会设置各个channel的标志位,如果condition等于0,会将对应channel的 Z标志位设置为0,否则设置为1。
3.语句3中,mov会利用语句2中对Z标志位的设置结果,更新各个channel的execute值。如果Z标志位等于0,则将@else_block的地址跟新到execute中。
4.语句4,根据execute的值,更新Z标志位
5.语句5根据以上4句的运算结果,如果全部channel的execute都不等于0,则跳转到@else_block地址。
   具体的说,如果全部channel的condition等于0,那么所有channel的execute都等于@else_block,所以语句4的执行后,所有channel的Z标志位都不为零,语句5满足跳转条件,跳转到@else_block;
   如果全部channel的condition都等于1,那么所有channel的execute等于0,所以语句4的执行后,所有channel的Z标志位都为零,语句5的跳转条件不满足,进入@then_block;
   如果各个channel的condition不全相等,那么,对于condition等于0的channel的execute值就等于@else_block;而对于1的channel,execute的值等于0,所以语句4的执行后,所有channel的Z标志位不全为零,语句5的跳转条件不满足,进入@then_block。
 
6.从语句6开始是if-then语句块。一旦能进入if-then语句块中,就需要依据各个channel的execute值,来判断指令能否被执行。所以每条指令执行前都要用execute更新一次Z标志位(暂不考虑优化),且每条指令都要加上条件执行码,例如:
    Mov.sf    null, execute
    Add.zs    tmp4, tmp3, tmp2

    Mov.sf    null, execute
    Sub.zs    tmp5, tmp3, tmp1

如前文所述,对于execute等于0的channel,执行“Mov.sf    null, execute”后,Z标志位置位,接下来的“Add.zs     tmp4, tmp3, tmp2”就能被执行。

7.在if-then语句块执行完毕后,离开if-then语句块前,需要判断执行流程的下一个入口。如5中所述,能进入到if-then语句块中有两种情况,分别对应不同的出口。
  1. 如果是全部channel的condition等于1,这种情况下我们接下来不需要执行else语句了。因为各个channel的execute均等于0,所以语句8执行后会把每个channel的Z标志位置位,紧接着语句9,也就把每个channel的execute值都更新为@after_block,在语句10测试跳转条件,与@after_block相减,结果为0,所以各个channel的Z标志位被置位,语句11的跳转条件是全部Z标志位置位,此时条件满足,即跳转到after_block,也就结束了if-then-else语句的处理。
  2. 如果不是全部channel的condition都等于1,接下来我们需要进入到else语句,执行非零的channel。语句8会把execute等于0的channel的Z标志位置位,语句9把Z标志位置位的channel的execute更新为@after_blcok,其余保存不变,语句10做条件测试的结果,不能满足语句11的跳转条件。接下进入else语句,并且这时各个channel的execute的值等于@else_blcok或@after_block。

8.能进入到else语句,也有两种情况,并且else语句中的指令也需要做if-then语句块中相同的处理,依据execute更新Z标准位,每条执行要交条件执行码。
   在执行else语句中的指令前,我们需要更新execute的值。只有在进入else语句前execute值等于@else_block的channel才能执行else语句中的指令。所以语句14先对execute做测试,与@else_block相减,对于execute等于@else_block的channel的N标志位会被置位,语句15据此更新execute的值,Z标志位被置位的channel的execute被更新为0。这样就能满足我们前提到的,只有当execute等于0的channel才会执行指令。
 
当else语句执行完毕后,我们就退出了if-then-else语句的处理流程。
 
LOOP
 
在SIMD指令下的loop语句块实现,与if-then-else语句块的实现方法类似。同样的,在loop中也会因为各个channel的情况不同,可能会产生控制流程分歧。当控制流存在分歧时,同样要依据各个channel的控制条件决定是否执行对应的指令,处理完分歧后,每个channel再汇合到一起。
 
这里考虑一种一般的情况,示例代码如下:
 
Loop {
 
    if (…) {
        …
        Break;
    }
 
    … …
 
    if (…) {
        …
        Continue;
    }
 
    … …
}
 
如果没有遇到break、continue语句loop会一直循环下去。而break中断并结束循环,continue中断本次循环,继续下一次循环(与C语言的类似)。
从上面的示例代码可以看出,loop中会产生分歧主要是源自于其中的if-then-else语句。当各个channel在执行if-then-else语句出现分歧后,在执行loop中语句就channel的执行情况就不一样了。
例如,当两个channel出现执行if语句出现分歧,A满足if语句块执行条件,B不满足,在if语句块中有break语句,if语句按前文所述的if-then-else语句处理分歧,A执行,B不执行,执行完毕if语句块后,A不再执行loop中的指令,B继续执行loop中的语句,直到满足推出loop的条件,最终与A汇合。
 
具体的实现与if-then-else语句类似,使用一个变量execute来记录执行条件,当channel的execute等于0时表示,该channel需要执行当前指令;对于不需要执行当前指令的channel,execute的值等于下一个需要执行的语句块的入口地址。
 
伪代码:
LOOP
 
01: mov execute, 0
 
02: loop:
 
03: sub.sf tmp0, execute, @loop_block
04: mov.zs execute, 0
 
05: …
 
/* inside an if block */
06: mov.sf null, execute
07: mov.zs execute, jump_block       /* jump_block = @loop_block or @break_block */
08: sub.sf null, execute, jump_block
09: branch.all_zero_set jump_block
 
10: ...
 
11: mov.sf null, execute
12: sub.zc.sf null, execute, @loop_block
13: branch.any_zero_set @loop_block
14: loop_end:
 
15: break_block:
 
1)
进入loop语句,首先对各个channel的execute进行测试,等于0的channel可以执行loop语句块中的指令。
同样loop语句块中的指令依然需要通过在每条语句执行前添加“mov.sf null, execute”来设置Z标志位(赞不考虑优化),每条指令添加条件执行码。
语句1,对execute初始化。
语句3,对execute测试,在进入loop前如果execute等于@loop_block,表示该channel会遭loop中执行,Z标志位被置位,然后语句4将对应channel的execute更新为0,反之,保存execute原值不变(初次进入时execute保持为0值)。
这样loop中的指令语句就能根据execute的值,判断是否被执行。
 
2)
当遇到if语句时,if语句的处理与前文所述的不变,只是在进入if-then-else语句块前不再将各个channel的execute初始化为0。
 
3)
当if语句中存在break指令时,并能执行break指令,对于满足if语句块执行条件的channel,将在break执行完毕后,结束loop语句块的执行,这些channel的下一个执行语句块是@break_loop(即退出loop语句块的指令语句地址)。而不满足if语句块执行条件的channel将继续执行loop中的指令语句。
语句6、7首先完成对break的channel的execute进行更新,这些channel下一个要执行的语句块地址为@break_loop,这样在接下来的循环体中,如果没有发生共同跳转,那么剩下的loop语句块中的条件执行指令,均不能满足执行条件。
语句8、9是为应对当所有channel均执行了break指令的情况,这时所有channel同时结束loop语句块的执行。具体来看,首先对更新后的execute进行测试,语句8与jump_block(break_block)相减,其结果会影响Z标准位,如果所有channel均置位了Z标志位,语句9的跳转条件满足,所有channel将共同跳转到break_block(break_block),也就跳出了loop语句块。
break指令是loop的唯一出口,所以各个channel一定会从某个break语句结束循环(死循环除外)。
 
4)
当if语句中存在continue指令时,并且continue指令能被执行,对于满足if语句块执行条件的channel,在执行完continue指令后,中断本次循环的执行,重新跳转到loop的开始处@loop_block,继续执行下一次的循环。
与break的情况类似,同样可以通过伪代码的语句6-8来处理continue指令,语句6置位测试条件,语句7根据测试结果设置新的跳转地址 @loop_block,语句8、9同样会判断是否所有channel的执行情况相同,相同时时直接开始下一次循环。
当各个channel的情况不一样时,还会接着loop语句块往下执行,同样的执行了continue的channel的execute已被更新,且不为0,loop语句块中剩余的代码也不会被这些channel执行了。
在完成一次loop语句块中代码的执行后,后开始新的一次循环前(如1中所述),均要做条件测试。
 
执行完一次loop后,语句11根据execute当前的值对Z标志位进行更新,语句12是条件执行指令,并且满足执行条件的channel会更新Z标志位,而不满足语句12执行条件的channel的Z标志位保存会不变(处于Z标志位置位状态)。
满足Z标志位未被置位的channel,会将execute与@loop_block相减,并更新Z标志位,对于执行过continue的channel,Z标志位会被置位,语句13,branch的跳转条件是任意channel为Z标志位被置位,就会回到loop的开头处@loop_block,继而进行下一循环.
这里能满足语句13跳转条件的channel,来至两种情况,一种是在循环体中未执行过break和continue指令的channel,execute一直保持为0,在语句11中置位Z标志位,且语句12不会清除Z标志位;另一种是执行了continue指令的channel。
所以在执行下一次循环前,各个channel的execute的值可能是0或@loop_block,且一定有某个channel的值为其中之一。
重新开始循环后,会先按上述1中的方式更新execute,然后就是再一次的循环体代码执行了。
 
总结一下
1.控制语句块中,执行每条代码前必须做条件测试(不考虑优化),每条代码必须按条件执行。
2.进入控制语句块前需做语句块执行条件测试
3.退出控制语句前,需要更新各个channel的跳转地址
 
参考资料:
mesa:src/gallium/drivers/vc4/vc4_program.c
https://people.freedesktop.org/~cwabbott0/nir-docs/intro.html
12-10 15:37