2010年5月3日星期一

LPC JIT优化

这段时间我抽了些空,为LPC driver开发了JIT功能。

我花了两天时间完成了编码,在Windows下调试好以后,一到Mac上果然立刻就崩溃了。系统提示错误:“misaligned_stack_error”

我查了一下资料,原来MacOS的内核要求进入时栈必须保持16字节对齐(因为内核有几个要求16字节对齐的指令操作),这真是有些不方便 - 因为在32模式下所有的call都只推进4个字节的eip,也就是说一旦进行call调用就会出现不对齐的情况。我查看了一下gcc编译生成的代码,原来它在进入函数以后,除了推进ebp以外,还会自动保留一段内存作为继续调用下一层函数的参数buffer,这个buffer的大小等于16n+8。这样加上ebp和推进的eip个字节可以保证堆栈是16字节对齐的。

比如某个函数A在进行下一步调用(如X、Y、Z)时,倘若最多只有两个参数的话,这函数A在入口处会自动保留2个32位的字。

sub esp, 0x08

这样,当进行X、Y、Z调用时,无需push参数,只要简单的设置堆栈中的值作为参数,然后进行调用。比如:

mov [esp + 0],
mov [esp + 4],
call

这样,在返回时还可省去了add esp, 8这样类似的出栈语句,倒也颇为方便。

在解决了32位driver的对齐问题以后,开始调试64位的driver。

本来迁移到64位不该是一个复杂的工作,只要调整一下相关的指令即可,但是没想到花费的时间却远远超出我的预期。

首先,我对AMD64指令并不熟悉,做了一下相关的试验以后发现和我预想的有些差别。其思路并不是像16位到32位那样简单的把指令从32位扩展到64位,很多指令仍然是和32位的立即数打交道,只有极少数指令是针对64位立即数进行操作。我想了一下,这是正解。如果过于频繁的对64位立即数操作,会浪费代码空间,而大部分指令实际上并无访问64位立即数的需求。

指令系统本身并不是太大的问题,只要熟悉格式而已。但是当生成64位指令以后,一执行即告异常。我研究了一会,才确认错误的原因是生成的代码在数据段没有执行权限,只是给出的异常信息有点古怪,一时没有诊断出原因。这点不难解决,用mprotect/VirtualProtect调整页面权限就可以了。

接下来的才是大麻烦,在64位系统下,gcc并不是像C语言传统方式那样用栈传递参数,而是用rdi、rsi、rcx、rdx、r8、r9等寄存器来传递参数,这应该是x64指令增加了8个通用寄存器(r8 - r15),有点类似RISC,寄存器数量比较多,所以采用了这个方案。然而Mac和Win64采用的寄存器顺序不一样,另外不同的编译器对寄存器的处理是不同的,有一些寄存器编译器并不保证在函数调用以后不被破坏(比如几乎所有的x86编译器都使用eax/rax作为返回值,若调用函数,这个寄存器原有的值就会被破坏),关键在于不同的编译器的方式不一样,在这一霎那我真希望世界上只有一种格式,那就是标准。不知道有多少程序员的生命燃烧在这些毫无意义的工作中,不停捣腾各种各样的差异。

Mac上的64位刚调通,返回去32位的却又不通了,没有对齐的问题又冒了出来。我研究了一会不得其解,按照经验,我相信这应该是一个非常弱智的问题。但是很疲倦了,这时候最好的做法就是先休息。

睡了一觉继续干活,看了看,果然是很弱智的问题。因为gcc在编译汇编代码时,可能会自动插入一些指令 - 比如试图进行访问变量、函数地址的时候,gcc会自动通过计算,将结果放到某个寄存器(比如ebx)中,为了避免eb被破坏,gcc还生成了一句push ecx,导致堆栈没有按照我的预期对齐。

考虑了一下,我决定手工书写汇编代码,而不使用内嵌asm语句,因为不同的编译器在处理内嵌汇编指令的方式也是不同的,索性用机器码硬写反而会更简单一些。事后证明这个决定的确是正确无比,因为VC 9的cl根本不支持x64下的汇编。

在调试完Mac下版本以后,继续回到Windows下测试。32位模式很容易解决,64位果然又很麻烦。首先我连编译都编译不过去,查了半天才定位出问题,原来是x64的C编译器cl优化有问题,在做某些优化时编译器会卡死。我很纳闷微软究竟有没有准备好面对64位平台,XP 64已经面世很多年了,没想到VC9的cl即不支持内嵌汇编,优化也没有做稳定。我试着打了一下VC9的SP1(这个安装过程奇慢无比,我真不知道这个安装程序都要做啥),结果没有任何改进。

我想试着切换到Linux下先完成这个平台的测试,没想到我在vmware fusion虚拟机里面安装了64位的ubuntu 10.04以后,键盘无法输入,我连登录都无法完成。我又尝试下载了ubuntu 9.1的x64版本,没想到vmware却不认识这个操作系统,安装失败。而32位的unbuntu 9.1在编译时遇到了一个问题,缺少g++的组件。我顺手就更新了一下ubuntu,没想到更新以后原先通过vmware共享的文件夹无法找到了。这一刻我真是要崩溃了,黎明前的感觉真是无比的黑暗,又一天浪费了。

再睡了一觉,继续。重新安装32位的ubuntu的vmware tools后,解决了共享的文件夹问题。接下来我关闭了VC9 for x64的优化,完成了调试。

下午回到厦门,我重新安装了Ubuntu 10.04 x64版本,键盘的问题其实是一个大众化的问题,选择手工安装即可解决。安装好了以后测试一次通过,我确认了一下gcc生成的代码,和Mac 64位完全一样,很顺利。

优化性能总结(数字的单位是执行时间,ms):

发件人 doing's board


注:Linux32和64采用的是虚拟机方式,Win32采用的是实体机。

注:没有Win64的测试结果,因为这个无法开启优化,测试数据意义不大。

这次优化对简单运算的效率提升效果明显,但是对外部函数调用等复杂操作没有什么效果,因为后者大部分CPU消耗在C语言而非虚拟机中。总的来说,这个优化对服务器端会有明显的效果,但是对客户端影响很小(就目前客户端的应用来说,脚本本身占CPU很少,任何针对效率的优化都没有太大意义)

由于在最初设计虚拟机时没有使用渐进式的垃圾收集方式,而采用了引用计数,这导致JIT的实现受到一些限制。如将频繁判断引用,效率比较低而且会使得生成代码太大。另一方面,当初我没有限制类型限制,声明变量时使用的类型更多用于注解而不是限制,而无类型的运算在编译成本地代码时会太长,并且效率也不高。如果采用类型限制,那么在计算方面的效率应该可以提升一个数量级左右。

最后,如果一切均有标准,我想只要2、3天就可以完成这次工作,但是实际上我花费了一周的时间才告一段落。不过,为了建立标准,也许付出的代价比所有程序员浪费的力气还要大得多,如果这样,那就让这个世界继续混乱下去吧!

ps:linux桌面应用已经到了可以接受的地步了。Firefox、Chrome浏览器均可以保持和PC一样的功能和风格,常规使用的办公软件、开发环境也很不错,如果必要还可以通过虚拟机启动Windows操作系统。

ps:Mac默认4个桌面,刚好配一个宿主机、三个虚拟机(Windows、Linux32、Linux64),倒是挺合适的。

ps:Linux在虚拟机下运行driver的性能比Mac在物理机上还高,虽说虚拟机在进行运算时损耗已经相当低了,但是我还是很纳闷:做Mac操作系统的这帮人是怎么搞的?

ps:就做服务器而言,Linux的确是一个很好的选择,不论是32位还是64位。

没有评论:

发表评论