代码单步展开

来源:互联网 发布:活动目录 知乎 编辑:程序博客网 时间:2024/05/22 14:45
结合前面lambda的例子,列表解析式看上去也可以这样做出python中的效果:
func main():    i = [0]    l = [for i[0] in range(10)]
这样就可以把解析式循环中的值带出来了,不过larva并没有实现这个语法,强制规定了for的左值表达式只能是简单的变量名或只有变量名组成的解包列表,因为我觉得还是规定为临时变量和当前环境没有互相影响比较好


抛开人为规定的因素不谈,假如想在代码转为java的实现方式下,这两个特性在语法和效果上都和python一致,也是可以做到的,至少有两种方法,第一种比较简单,既然前面说了用列表可以做到,只是语法上需要显式将变量实现为一个元素的列表,那么只需要语法上做个转换就可以了,也就是说对于这个代码:
[i for i in l]
编译器检测到i在解析式中作为左值出现,则将i自动转成一个容器,当然所有用到i的地方也都改成容器引用的方式,这个容器不用在larva语言层面实现,直接在java中new LarObj[1]即可,对于lambda也可以有同样的做法


不过这个做法有两个小问题,第一,若全局变量定义中出现解析式:
g = [i for i in range(10)]
这个时候按照python的规定,i也是一个全局变量,即命名空间和g同级,当然larva也可以这样实现,将i视为一个全局变量,不过这种全局变量的定义方式不是那么明显,这也是语法上规定解析式左值必须是临时变量的一个原因


第二个问题是在生成代码中,如果将变量改为大小为1的数组,会造成执行效率上的问题,比方说一个很大的函数中很多地方都用到了变量i,而仅仅因为一个列表解析式就导致所有直接访问i的地方需要变成一个数组下标访问,java的数组下标访问需要检查越界,还是比较慢的,而且这种访问会跨栈和堆两块内存,局部性上也存在问题,当然,如果是支持指针的语言,比如C或go,就没这种问题了
这个效率问题还体现在另一个java的实现机制上,大量小对象会给GC带来一些负担,不过如果这些小对象生存周期很短,影响也不严重,这个问题在后面讨论列表和字典数据结构的实现时会再深入一下


假如一个函数像上面说的,某变量只在很少的时候作为解析式for的左值,大部分时候都是访问,则我们可以采用临时变量的办法:
a = [i for i in l]
改成:
tmp_i = [0]a = [tmp_i[0] for tmp_i[0] in l]i = tmp_i[0]
这样就可以在牺牲这几行代码的局部效率的代价下提高整体性能,但这么做的问题是,一个表达式会被拆成多个语句,如果解析式在一个很复杂的表达式中,就有可能无法直译,因为这个表达式既要求值,又要实现临时变量赋值、调换之类的副作用。当然,java的赋值是表达式,且有逗号运算符,合理安排代码的话也是能实现的,但我认为这种做法比较ugly


于是就有了第二种方法,修改解析式的实现,不将其转成一个函数调用,而是直接内联为一段代码,这样一来,内联的代码就可以直接修改当前环境,简单说就是:
a = [i for i in l]
编译器自动将其展开:
a = []for i in l:    a.add(i)
而如果表达式比较复杂,就需要临时变量,比如:
a = [i for i in l] + [i for i in m]
定义两个临时变量tmp_1和tmp_2,分别展开两边的表达式,最后a=tmp_1+tmp_2


从另一个角度考虑,如果larva的实现不是代码转换,而是编译为ast或字节码解释执行,则上面的问题也不是问题,因为字节码已经将流程单步展开了,反过来说,采用转为java的方式实现时,按照字节码的执行顺序展开成一个个简单的语句就可以了,实现上是等价的
不过,如果只是从字节码直译为高级语言代码,效率会打折扣,所以可以做一些优化措施,比如:
a = b + c + d
这个在栈虚拟机字节码中,需要三次入栈和两次运算,而如果直接转为代码,就无需显式实现一个栈,而可以用临时变量来做,因为这个语句执行过程中栈的变化方式是固定的:
tmp_1 = btmp_2 = ctmp_1 += tmp_2tmp_2 = dtmp_1 += tmp_2a = tmp_1
编译期就可以计算出,这句语句只需要大小为2的栈,而实际上入栈出栈操作都可以优化掉,一个临时变量即可:
tmp_1 = b + ca = tmp_1 + d
这样一来,生成代码中一个函数只需要较少的临时变量,即可做到代码单步展开,再进一步,java编译器也会进一步优化这些临时变量,所以单步展开对效率的影响是很小的


如果只是为了解析式,做单步展开多少有点小题大做,毕竟在larva设计中,解析式的左值作为临时变量是一个更合适的做法。代码的单步展开更大的作用是在其他方面,笼统地说,就是在代码直译无法实现的某些功能上,一般是由目标语言的功能所限制


例如,如果选择将larva转为java,由于java本身提供了异常机制,且与larva的异常机制非常类似,所以代码直译时不需要考虑这方面的处理,可以做到特性的一一对应,但是如果将larva转为C,异常机制可能就需要用返回值来做,比如调用一个函数f:
a = f()
转成C:
a = f();if (a == NULL){    ... //异常处理}
由于larva规定每个函数都有返回值,且返回一个对象,则可以用返回NULL来表示异常,对上面的代码这样直接转没有问题,就是插入异常检查代码而已,反正代码也是自动生成的,但是对于这个语句:
a = f() + g()
就不能直译了,只能采用单步展开的方式才能分别检查f(),g()和加法的异常(别忘了加法是实现为一个方法)


上面只讨论了表达式的单步展开,对于执行流程也可以展开,具体的,就是将循环、分支等展开为直接goto的形式,虽然java没有goto,但是可以用这种形式替代:
while (true){    switch(label)    {        case label_0001:        ......    }}
需要goto的时候,设置一下label,然后continue即可,由于java的switch一般是采用表跳转的方式,效率也不会有什么影响,当然也归功于switch case语句顺序执行的特性(没有break)


这样展开的一个好处是可以方便地在代码转换中实现协程,由于每个函数都是一个循环加switch,因此只要栈帧数据另外保存,就可以yield(实际上就是return),然后下次恢复执行序,其实,如果只是为了协程,则也可以分情况讨论,比如:
for i in range(10):    j = i * ifor i in range(10):    f() //假设f会yield
这两个循环中,只有第二个需要展开,第一个for循环不可能yield,所以可以不用展开,因为调度是非抢占式


更进一步的,甚至可以把函数调用都展开成栈帧管理和goto操作,然而这样一来等于是所有代码都内联,不利于直接用目标语言模块化编程(比如一些扩展库之类),所以我觉得意义不大


最后提一点有意思的,假设代码转成C,虽然用goto可以实现上述的while+switch结构,但我们有更方便的办法,因为C语言支持switch的case项越过循环代码块,比如:
switch (stat){    case A:        ...        for (...;...;...)        {    case B:            ...        }}
这实际上相当于直接往循环体里面goto,我见过的一些C++的协程编程就是这么干的,只不过是用宏来代替上述switch case,改成了更直观的方式,而java就不支持这种语法,代码块必须严格嵌套。另外需要说明的是,这种情况下的goto不可以造成一些执行流上的问题,比如在上面for语句括号中第一个域前面定义一个对象A a;,则直接从case B执行就跳过了a的构造而直接使用,对于这一点vs下会提示错误,不过如果是协程的实现,所有局部变量应该是存在栈帧对象里面,对于临时变量注意一下这个问题即可


P.S.对于正常的C编程,这些技巧当然是不推荐使用的
0 0
原创粉丝点击