这个问题在很大程度上是自己造成的,是由于编译器太慢(我们经常使用LLVM)造成的,而我们给它提供了大段代码来进行更多的优化,这让情况变得更糟。
通过限制自己一次只编译一条指令,我们在表上留下了一些性能,但极大地提高了编译速度。
这使得编译速度非常快,因为我们基本上只是在每次使用指令时复制-粘贴模板,仅根据其参数执行一些细微的调整。
乍一看,这可能令人望而生畏,但一旦你习惯了,实际上并不是那么糟糕。虽然即使是最小的事情也需要大量的代码才能实现,但只要代码保持简短,就本质上是简单和容易遵循的。
缺点是每种体系结构都需要实现每条指令,但幸运的是没有很多流行的指令,我们希望在发布OTP24时支持两个最常见的指令:x86_64和AArch64。其他人将继续使用口译员。
编译模块时,JIT逐个检查指令,并在编译过程中调用机器代码模板。这对解释器有两个非常大的好处:不需要在它们之间跳转,因为它们是背靠背发出的,每个参数的结尾是下一个参数的开始,并且参数不需要在运行时解决,因为它们已经“燃烧”了。
现在我们已经了解了一些背景知识,让我们来看一下上一篇文章中示例的机器代码模板IS_NONEMPTY_LIST:
/*参数作为包含*类型和值的`ArgVal`对象传递,例如说";X寄存器4";,*";原子';hello';";,";Label 57";等等。*/void BeamModuleAssembler::emit_is_non Empty_List(const ArgVal&;Fail,const ArgVal&;Src){/*找出`Src`位于哪个内存地址。*/x86:Mem list_ptr=getArgRef(Src);/*发出`test`指令,对*list_ptr指向的内存进行非*破坏性操作,如果列表为*空,则清零标志。*/a。Test(list_ptr,imm(_tag_primary_ask-tag_primary_list));/*如果零标志清零(列表为空),则会发出`jnz`指令,跳转到失败标签*。*/a。JNZ(标签[失败。GetValue()]);/*与那里的解释器不同,不需要跳到*下一条成功指令,因为它紧跟在这条指令之后。*/}
该模板将生成与模板本身几乎相同的代码。假设我们的源是“X寄存器1”,失败标签是57:
这比解释器快得多,甚至比线程化代码更紧凑,但这是一条微不足道的指令。更复杂的呢?让我们来看看解释器中的超时指令:
超时(){if(is_traced_FL(c_p,F_trace_Receive)){TRACE_RECEIVE(c_p,am_lock_service,am_timeout,am_timeout,null);}if(ERTS_proc_get_saved_call_buf(C_P)){save_call(c_p,&;exp_timeout);}c_p->;标志&;=~F_Timo;Join_Message(C_P);}。
这必然会有大量的代码,而手动转换这些宏真的很烦人。我们究竟如何才能做到这一点而不发疯呢?
静态无效超时(进程*c_p){if(is_traced_FL(c_p,F_trace_Receive)){trace_Receive(c_p,am_lock_service,am_timeout,am_timeout);}if(ERTS_proc_get_saved_call_buf(C_P)){save_call(c_p,&;exp_timeout);}c_p->;标志&;=~F_Timo;Join_Message(C_P);}void BeamModuleAssembler::emit_timeout(){/*将第一个C参数设置为当前正在执行的*进程c_p,然后调用上面的C函数。*/a。Mov(arg1,c_p);a.。Call(imm(超时));}。
这个小的逃生口让我们从一开始就不必用汇编语言写所有的东西,而且很多指令仍然是这样的,因为没有必要改变它们。
今天就到这里吧。在下一篇文章中,我们将介绍我们的约定和一些我们用来减少代码大小的技术。