if-then-else、loop控制语句的后端实现
本文是通过代码而来,主要记录了在SIMD指令集上,编译器后端对控制语句(if-then-else、loop)的指令生成方法。
引言:
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语句块中有两种情况,分别对应不同的出口。
- 如果是全部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语句的处理。
如果不是全部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