2009年3月31日星期二

PP的进一步结果

这几天改写了PP以后,今天终于将版本更新上去了。

再次运行PP,结果在收集了一小时的数据以后崩溃了一次,虽然这个在意料之中,但是还是感觉很不爽。因为编译时采用了omit stack frame pointer的优化选项,结果看调用栈没有任何收获。

重启一段时间以后再次运行PP,这次为了保险起见,只运行了5分钟,从统计的角度来说也够了。

这次PP比上一次统计的更精确一些,首先是30us一次,其次可以统计到外部函数的调用情况(上次PP有一个bug,没能做到这一点)

从这次结果中可以看出,服务器有几个比较明显的开销地方。一处是bzip压缩开销很大,还有一处是逻辑上的缺陷,在处理外观数据时居然每次都处理了一下配置表,结果开销浪费很大。除此之外,还有一些逻辑上的问题,做了一些无用功,重复处理导致浪费。预计这些简单的问题解决以后,服务器端的性能可以提升10-15%左右。如果能进一步解决掉一些比较复杂的问题,还有希望再提升10%。

从这段时间的进展来看,工具可以提升很大的效率。因此选择、开发合适的工具很重要,不过很多开发人员都忽视了这一点。

绕过ReadConsole阻塞的问题

今天我考虑了一下,换了一个角度来看ReadConsole阻塞的问题。

本质上来说,目前我用ReadConsole只是读取一行,中间可能会插入其他读入(比如常规控制台在读入的时候,debug控制台起来了,需要终止控制台输入),采用抢占输入的方式即可。换言之,当ReadConsole完成以后看看是否有人抢占,如果有,则将结果交给抢占者,自己继续读取即可。

采用这个思路以后,问题就简单了,我没有必要考虑如何终止ReadConsole。现在启动debug调试时就舒服多了,不会和控制台冲突。而且,这个解决方案是跨平台的,我也不需要考虑其他系统下是否会有什么兼容性的问题。

2009年3月30日星期一

如何中断ReadConsole

在做debugger的时候,有一点令人感觉不爽。

在界面输入命令时,在Windows下使用的是ReadConsole,而我现在不知道如何用一个简单的方法打断这个输入。那么,在用户开启控制台输入时,如果进入调试模式,不能自动打断这个调用结果令人感到不愉快。因为必须输入一行内容才可以继续调试。

虽然使用异步的方式,不用LINE_INPUT模式可以解决此问题;或是另外开启一个线程进行读取操作。但是不论如何,都是不够轻便,不够简单。

2009年3月29日星期日

这两天考虑增加一个命令行的debugger用于调试LPC脚本。

主要让我觉得需要斟酌的一点是:当断点断下时,是挂起整个VM线程,还是只挂起脚本执行,线程依旧。如果是后者,实现起来会有点麻烦,并且需要另外制作一个debugger使用的调度器。而采用前者,会让系统的栈有点深,而且在scan sockets方面的代码会有一些重复。而且在调试时不能进行关闭VM的操作 - 原先是可以这样的。

在权衡的时候我考虑了另外一点,如果VM线程没有挂起,就会导致渲染等操作仍然进行,这种异步操作可能会给调试带来困扰,基于这点,我选择还是让VM线程挂起。

上午开工,比我预期的速度要快,毕竟做一个调试器并不算太麻烦。以前我在做i386的虚拟处理器的时候,制作调试器很容易。目前要做的工作比之前主要麻烦在watch和file这两方面,因为虚拟处理器只需要查阅寄存器和内存即可,也没有文件的符号信息这一说。

晚上还没有完全编码结束,考虑明天工作可能很多,今天晚上就暂时告一段落,明天抽空用几个小时应该可以结束编码,最后调试的时间预计不会太多。

2009年3月28日星期六

轻重缓急

这几天休息不好,晚上有点困就早点回去了。

在家里上了一会儿问鼎,看到一些问题。这些问题并不是新出来的,可以说是由来已久的,而且也不应难解决。但是到现在迟迟未能得到解决,究竟是有更重要的任务占了先,还是说安排的不妥当?没有分清轻重缓急?

以我以前对项目的观察来看,估计是后者居多。确定优先级看上去并不是想象的那么简单,这是一个很重要的技能。

2009年3月27日星期五

调试手段

自从我完成了LPC driver并投入实用这段时间以来,其最大的问题就是缺乏调试工具。这使得开发的时候调试很不方便,经常要手工增加printf,然后update object查看结果。当年这么开发还好,但是随着项目越来越复杂,问题也越来越多。

我本来打算针对VC这样的调试终端开发,让VC可以直接连上driver调试脚本。但是因为一直没有时间专心研读这方面的资料,所以这个工作迟迟没有开展。

前两天调试一个问题的时候,感觉工具带来的低效越发的明显。于是我先实现了动态设置陷阱的方法,利用这个功能,我可以方便的在任何一个脚本函数、外部函数上设置陷阱,并且实时取得当时执行的信息。虽然这个离终极解决方案还颇有距离,但是已经是大大的向前跨了一步了。

为了避免这个功能影响正常脚本执行,我为VM增加了一个debug模式的虚拟处理器。当有陷阱断点被设置时就切换到这个虚拟处理器,而当处于非调试状态时则使用常规的虚拟处理器。这样正常运行时就不会有任何性能上的损失。

随着这方面工作的完成,也许我该考虑先完成一个文字界面的调试工具了。

2009年3月26日星期四

提升PP的性能

因为昨天出门很失败,于是今天我试验了一下us级的PP,果然,对深度调用的情况影响很大。

我看了一下代码,主要是因为同步引起的。在进行profile的时候需要进入临界区,VM在处理调用时(进、退调用栈)也会进入临界区,这样就保证了profile的时候不会发生栈变化的情况。

然而,我试验了33层长函数名的调用栈profile的情况,需要17us,锁住临界区如此长的时间,显然有问题。于是我优化了一下这部分的代码:生成调用树主要是依赖于函数的名字,每次根据函数名进行哈希查找成本有些大,于是我改为根据函数名的指针进行哈希查找。但是这样引发了一个新问题,因为脚本是可以动态卸载的,如果函数被释放了怎么办?我采用的方案是另外记录一个mapping,根据函数名指针可以朝着到对应的函数名。每次在记录调用栈中的函数名时,如果这个指针已经存在了,则忽略,否则就先记录这个指针对应的函数名。

实际上在做profile的时候,几乎不会发生动态析构脚本的情况,直接使用函数名指针效率是最高的。但是我并不希望这个profile依赖用户的行为(比如他无意进行了析构,可能就会导致问题)。我这样做仍然有一个微小的隐患,如果用户析构了脚本函数又重新加载了,新的函数名指针恰好和原先的函数名指针相等的话,统计就会记录错误的名字 - 我认为这并不重要。

另外,我增加了一步:复制调用栈,在临界区内我只复制调用栈,然后在临界区外进行生成调用树的操作,这样就尽可能的减少和VM的冲突了。为了防止在生成调用树的时候发生脚本析构的情况,我另外增加了一个生成调用数时的临界区,在析构脚本的时候VM会进入这个临界区,避免发生不同步的情况。

优化以后速度快了20倍,毕竟每次哈希都不需要处理字符串,而是处理一个64位的整数即可(因为除了函数名,我还需要记录对象名,各32位)。另外临界区冲突的可能性则更是大大的降低了。

但是性能仍然让我不够满意,这还是没有达到1us级别的统计性能,只能进行10us级别的统计。因为统计33层调用本身需要0.7us左右,在这种情况下进行1us间隔的统计意义并不大,因为每次采样之间波动可能就会达到1us。

我暂时采用限制PP最低以10us的间隔进行采用,将来再考虑如何进行进一步优化。现在还有一个问题,就是PP以10us的间隔进行采样时,VM性能大概会降低30%-40%,主要是函数调用会受到影响。如果关掉同步自然皆大欢喜,速度立刻恢复正常。但是我始终不愿意这样做,虽然这样只是会让统计结果稍微有些偏差而已。

公司今年5周年庆,不知不觉,已经过去5年了。以目前的成绩来看,和想象还很有差距啊!

2009年3月25日星期三

PP投入了实用

下午和蓝港打了一下招呼,我准备开始在服务器上采集数据,毕竟这个模块比较彪悍,还没有真正投入过实用,没准会引起崩溃什么的。

4:30开工,没想到一开始就掉了链子。我们每us采集一次调用栈,结果服务器卡的不行。因为driver出栈的时候会进行同步,如果正在采集中,需要等待本次采集结束以免数据错乱。赶紧改成每ms一次,只要采集的时间长点,倒也没有问题。

5:00去打球,锻炼身体。晚上冲了个凉,满怀期待的心情来看结果。

没想到又掉了链子,因为调用栈太深,所以生成的调用树很深。结果试图用save_string进行保存将mapping转为字符串的时候遇到了问题 - driver只能保存31层的mapping,而这个配置还不能动态修改。没办法,同事只好写了一个脚本进行特别处理,将这个mapping保存下来。

接下来将数据导入viewer查看,再次掉链子。因为数据太大了,结果导入速度实在太慢,结果只好先毙掉了一部分功能暂时先看看结果。

总的来说,因为这块经验不足,设计和实现的时候有太多和实际偏差的地方。还需要总结经验,根据反馈进行改进。

当然,结果很令人满意,之前手工统计的内容在PP的报告完全可以体现,一览无余。另外PP能够揭示更多的问题,让我们有了入手核查的地方。总的来说,这个统计方式算是CPU占用的终极解决方案。接下来两周,我们将有重大突破。

工欲善其事,必先利其器啊!

2009年3月24日星期二

锻炼了一下身体

今天晚上有同事组织去打羽毛球,去练了一个小时。感觉精神有所好转,算是储备了一点MP。

这两天进展不错,美术资源整理完毕;同事解决了一个导致频繁崩溃的问题;我也干掉了一个出城重复创建资源导致有点卡的问题。另外关注到了两个表现有点卡的地方。而且测试用的破机器也装好了,实验了一下,解决了一个兼容性的问题。因为老的显卡驱动不支持创建和锁定dynamic texture,目前我们只有光标和字体需要用这个特性。将光标改由系统默认的光标,字体则更换为static的(据称性能会差很多),就能跑起来了,有13帧左右,表现可以接受。不过是切换地图,战斗的地方需要考虑如何做的更流畅。

蓝港考虑到版本稳定和广告排期的问题,通知将延期一点时间进行大规模内测。不过我还是要求开发团队按照原计划制定稳定版本,即Apr/5完成待发布的版本,再沉淀5天。毕竟事情赶早不赶晚啊,要延误也不能停留在我这个环节上。

2009年3月23日星期一

Performance profiler

driver以前主要是针对内存进行统计,一直缺乏对CPU占用率的有效统计手段。虽然可以取得脚本线程占用的时间,但是因为大量短寿命线程(如callback)的存在,加上线程内的复杂执行情况(函数嵌套),所以仅仅取得线程占用时间的作用很有限。必须手工增加很多统计代码才能实用。

考虑了一段时间,上周末的时候我借合并版本的空闲增加了一个性能诊断模块。

统计的主要思路是独起一个线程,定期去记录调用栈,然后将调用栈插入到一个树中,形成调用树,每次给被统计的调用栈在树中的末端节点计数器+1。最终,计数器总和可以看作是100%的CPU,各个结点计数和计数总和的比就是占用CPU的比率。这样,我可以得到每个调用层次不同函数的CPU占用率,也可以得到它们调用其他函数的总开销(将这个分支下面所有节点的计数累加在一起即可)

当然,统计只是一部分,还需要一个viewr,要能方便的展开这个树,并且进行同层次的排序以便阅读,另外也可以进行一些高级分析。

在实际开发的时候,如果profiler的线程采用休眠的方式的话,每秒中统计的次数太少。Mac下基本可以做到每毫秒一次,我用的Linux就不太好,而Windows最差,因为精度太低每秒只有60多次。既然目前都是双核+的机器,我让profiler线程以不休眠的方式进行,每微妙统计一次。结果下来,还是Mac最好,Linux次之,Windows最差,因为QueryPerformanceCounter实在是开销太大了,一次调用就干掉几微妙。

合并版本

周末已经将最终用于大规模内测的版本制作完成了。按照惯例,我们是周一进行流delivery和rebase的操作。也就是说将之前patch的内容合并到集成流上,并将这些内容建好基线以便分发给所有的开发人员。

为了加快验收速度,以便早日发布,可以及早进行后期的稳定工作。我要求周日就完成这部分工作。

8:00就开工了,没想到夜里2:00钟居然还没有干完...

混乱的原因有待分析,不过这样效率真是底下。

2009年3月21日星期六

休息

今天比较顺利,QA完成了本周的版本的测试,所有bug都解决的都无惊无险。我也解决了昨天发现的driver的问题,同时为将来的更新也增加了告警并调整了目前的代码。

目前基本上所有的优化工作都已经完成,美术资源方面还有一些小尾巴,客户端有一些地方没有做到完全透彻,但是问题都不大,足以应对接下来的大规模内测。现在真正需要面对是稳定性的问题。

按照我的计划,周日将封闭最新提交的内容,这将是大规模内测的版本。下周QA将验收这个版本,而我们则集中力量解决在外面运行中版本的问题(也就是本周QA完成测试的版本)。如果在一周内基本解决了运行版本的问题,那么和QA下周测试完的版本进行合并,可以得到一个相对稳定的版本,我们还有一周的时间再去扫尾。如果一切顺利,2周以后我们就可以有一个符合要求的大规模内测的版本。

于是,我打算明天给自己放个假,休息一下,下周好几种精力收拾bugs。记得当年玩三国演义,若是选休息,就会有一句提示:"休息是为了走更远的路"。谁说不是呢?

今天是烧火的日子

早晨起来的有点晚,9:00才到公司,启动问鼎一看,客户端还没有更新。问了一下,原来是走路又出现了问题,我push了一下,发现解决速度果然很慢,第一个程序员还在琢磨怎么取版本(因为前一天更换了SVN上的流设置,有些变化),当下心头不爽。

接下来我自己看了一下走路相关的代码,发现运行中竟然不能更新此文件,更新以后无法正常工作,完全没有达到预期的设计目标,心头大怒,发了封邮件谴责程序组。

随后看到同事发过来的邮件,指出了一处底层代码居然有策划设定相关的逻辑。我一看狂怒,查了版本树,结果是亘古以来就有的(当年版本数据库迁移过,2008年以前的没有记录了),只好作罢。

中午去打水,发现水池中有一团米粒堵着,心头是怒不可遏,我最恨这类无卫生意识的人。基本上,我发送everyone的找麻烦邮件大抵都是挑卫生方面的毛病,居然在眼皮底下还有这种事情。可惜没有摄像记录,找不到责任人,我发邮件给everyone猛批了一通。

晚上听说“天下评定”活动不能正常开启。我找人问了一下,居然没什么人知道,只有一个程序说了这个bug已经发现并解决了。我就顺便问了一下解决方案,不想一听就觉得有问题:原先搞出毛病的那个人就是乱来,现在的修正方案则是补丁上的补丁,一团混乱,我狂批了半个小时。

唯一值得欣慰的是,昨天总算把服务器端的版本发了。今天将集成出一个无比混乱的大规模内测版本,下周将是集中精力排错的一个过程。如果下周能够顺利排除主要错误,那么内测的计划尚不至于受到影响。

2009年3月19日星期四

针对VM未来内存方面优化的考虑

我这里只考虑一些立竿见影,成本很低的优化方案。

目前每个值(value)都有一个引用字段,用了4个字节,我在考虑如何不用复杂的垃圾回收机制就能够消除这4个字节。我想了一下,能对值进行引用的只有VM中的栈变量。当对类似如下语句:
a = 7
m["x"] = 99
这样的语句进行操作的时候,会产生对变量a、m、值m["x"]的引用。而a、m只可能是全局变量、对象变量、局部变量这三种情况。针对于这三种变量,都无需考虑引用的问题。全局变量只在driver clean environment时才释放,对象变量在对象析构时释放,局部变量则在返回时释放,都不需要考虑引用的问题。关键在于改写容器内成员值时,有可能发生引用。

事实上这不难解决,我只要为对容器赋值做一些特别操作(比如set_member)即可,问题在于,我支持调用时传入引用,比如foo(&m["x"]),这给我带来很大的麻烦:我必须有一种可以传递容器内值引用的方法。虽然,我可以使用记录容器和容器内索引的方法,但是我仔细思考了一下,这样有点浪费效率(在处理赋值、传递引用指针都会慢一些),并且代码有点复杂,不是正途。

说实话,如果现在能够自由的做决定,我就干掉函数调用时传递引用这种行为,因为很少有需求要使用。然而现在我应该保持和以前代码的兼容性,毕竟我不希望将来问道试图升级driver的时候花费太大的经历。

我从另一个角度考虑了一下,可以对这种引用做一个记录。每次进行这种引用时,就将被引用的指针加入到一个数组里面。当取消引用时则遍历数组然后删除 - 听起来这样开销很惊人,不过实际上,代码中几乎没有对非变量的普通值的引用,所以效率影响微乎其微。

在一个值被释放时,需要检查一下是否在引用数组里面,如果是,则先不释放 - 等到引用消除时在释放,否则直接释放。为了提高速度,我给值保留一个比特的是否被引用字段,因为目前一个值除了引用还由2个32位数表示,找出1位不是难事。这样就可以将值压缩到8个字节。

算法描述:
当对值进行引用时,即I_QUERY_MEMBER_PTR指令,需要进行如下操作:
SET value.attrib.ref = 1

当对引用进行赋值传递时,则:
IF src_value.attrib.ref == 1 THEN
PUT src_value TO ref_array (此时ref_array有两个重复的src_value)
END

当清除引用时,则:
IF value.attrib.ref == 1 THEN
REMOVE value FROM ref_array(此时清除的一般是后面增加的value,这样速度更快)
IF value.attrib.free == 1 THEN
FREE value (原先容器试图释放该值,清除引用)
END
END

当值被试图释放时,则:
value.attrib.free = 1
IF value.attrib.ref == 0 THEN
FREE value (大部分情况都会如此)
END

为了应付最坏的情况,我需要准备一个size能够包容所有VM线程栈变量的数组 - 虽然几乎没可能会用到这么大的情况。但是在赋值的时候出现数组溢出是一个很糟糕的问题,为了简化代码,保证稳定性,这点空间应该无足轻重(大概是线程数x1K空间即可)

当然,我还有另外一种比较简单的做法,就是从8个字节的value中找出16位作为引用。但是这不太可靠,如果非常不幸的,对值的引用超过了65535(比如有意让栈变量全部去引用这个值),可能会造成意想不到的后果。

更新不顺利并顺利着

昨天感冒,身体颇为不适,于是9:00就先回家休息了。

半夜3:00钟的时候,QA给我打电话(我已经把手机调成了震动,但是我睡觉很轻,即使这样仍然可以唤醒我)。我当时接起电话就知道不妙,果然,服务器和客户端都出现了一些诡秘的bugs。我本想当时就过去解决,但是感觉很不舒服,估计过去效率也很低,还是先安心睡觉。

一早起来,感觉有所好转,用完早餐,8:00就赶到了公司。查看了一下QA留给我的邮件,针对bugs进行排查,还算顺利,9:00前就解决了2个打包版本的问题。令我我有些郁闷的是:之前我没有针对服务器端的打包版本进行测试。我只测试了客户端的Debug、Release x 打包、不打包四种情况和服务器端Debug不打包版本,但是服务器端的打包版本就没有测试,今天果然遭了报应。

由于现在都是QA打包发布版本,结果服务器端工程配置文件年久失修。工程文件中的打包规则基本不可用,我花了半个小时才将缺漏补上。所以说,很多东西只要不用就会慢慢的被腐蚀废弃,如何让它们保持新鲜是一门值得琢磨的学问。一般来说,自动化的测试可以在很大程度上弥补这一点,我们自动化测试的工作还有很漫长的路要走。

上午要开企业文化会议,这个不能耽搁,虽然手头的问题很多,但是我并不打算改期,毕竟磨刀不误砍柴工 - 于是一个上午都在磨刀。

下午尽量集中精力,将服务器端、客户端 x Debug、Release x 打包、不打包全部测试了一遍。因为嗑了药,头比较昏,有些问题思虑不周,反复了几次才彻底解决。

将上述工作弄完以后,再查看QA提出的另外两个诡秘的bugs,研究了一下,果然都和driver有关,一一修改。总算在晚上将所有工作提交并且制作出了发布版本。至于是否能发布,就要看接下来的运气了。

另外,今天同事尝试用stlport替换了VC自带的STL,debug版本速度快了很多,将来客户端总算可以回归debug版本,以便将更多的bugs消灭在实验室中。微软的STL库乱七八糟的判定实在太多,本意虽好,结果是完全不可用。虽说可以通过配置调整,不过,在有stlport的情况下,真的有必要那么做吗?

2009年3月18日星期三

更新中

昨天发布了版本,今天出了几个比较严重的问题。

晚上我一问,居然还没有更新解决问题。这个版本的确问题比较多,比如以前driver在限制const的时候处理的有点问题,导致某些情况没有诊断出来,脚本可以乱写,新版本的driver修复了这个问题结果不少脚本受到影响。但是,我仍然认为我们反映速度太慢。就这几个问题而言,应该可以短时间内解决,实在不能解决可以先绕过,尽快更新。无论如何,拖延到晚上还没有解决是不应该的。

昨天搞的有点晚,今天早晨想休息一下也未遂,附近太吵了,兼有些感冒,所以9:00点就先回来休息了。但愿明天我过去的时候已经将问题搞定了,阿门。

提交-最终战役揭幕

本来今天就可以提交了,但是我昨天晚上想了一下,仍然有些立竿见影的事情可以做。

目前driver使用了38M内存,但是占用空间达50M,这是管理开销引起的。系统在启动的时候,需要加载大量的脚本bytecode,加载的过程中申请了很多小片内存,但是显然这些内存在整个生命周期内都不会被释放。所以,我引入了稳定堆的做法,准备一块平坦的内存,这些小片内存都优先在这个堆上申请,它们不会被释放,从而达到节约管理开销。

为了做到这一步,我首先整理了一下driver在编译、加载bytecode阶段的代码。将稳定的内存和临时使用的内存分开,这花费了我一个上午的时间。下午又花了点时间顺便优化了加载bytecode的过程,给它加了一个缓冲 - 本来我认为没有必要,因为driver是将整个bytecode文件读入内存然后分析的,但是考虑直接从driver封装的io读取内存数据额外CPU开销还是有点大,所以仍然做了一个本地的小型缓冲,4K。效果还不错,启动速度提高了15%左右。

将申请分开以后,我就开始着手添加稳定堆。这个实现非常简单,就是事先准备好一段内存,当申请的时候简单的将指针累加返回即可。申请的速度非常快,而且额外开销只有0-3字节,这是为了确保对齐。实现本身很简单,不过我还需要增加相应的profile功能 - 事实上,只实现功能只能算做了一半的工作,没有足够的profile方法很难进行维护、优化和各种各种诊断。

采用稳定堆的做法并没有减少任何直接的内存使用,但是管理开销减少了3M多,和我预期的差不多。最终客户端启动只占119M,想想一个月前客户端启动就是210M内存,还算是欣慰。driver占用的内存从130M下降到了43M,节约了90M左右。

晚上开始提交相应的库,这个版本我将尽快更新到外网,以便发现稳定性方面的问题。也不知道该说运气好还是不好,刚刚提交我就发现了一个问题,花了点时间解决。

总的来说,今天的优化工作只是锦上添花,实际意义不大。因为目前已经到到了设计目标,从工程角度来说,并非要做到最节省才好。只是出于个人爱好,我又多花费了一天的时间。

最后:从实现逻辑的角度来说,脚本代码并不比C代码更浪费内存,甚至可以说会节省一些。在STL被广泛使用的今天,C语言的代码急剧增加。脚本代码在处理逻辑的时候,因为指令功能较强,实际上表达相同的逻辑,使用的指令并不会比汇编指令多。当然,使用脚本占用内存的真正问题在于变量上,由于需要携带类型,所以在大量处理整数时,脚本浪费的较多。

2009年3月16日星期一

优化尾声-打扫战场

在完成继承方式的修改以后,针对driver节约内存的主要工作已经差不多结束了。

接下来我开始从driver内置的内存分配统计工具入手,检查有哪些地方会申请比较多的空间。发现了一些针对服务器端的设置。比如用于封装数据包的缓存size高达256K,允许最大的连接数是2048等等。在客户端做了针对性的配置以后,这部分内存占用基本被压缩到无。当然,由于它们总共加在一起也没有多少,所以这部分效果不算显著,基本上就是打扫战场的行为。

这部分工作完成以后,客户端登录进入游戏以后占用的内存是130M左右,比原先的220M少了近90M。这部分节约主要来自于driver的优化(内存占用从109M下降到39M,另外节约了10M的管理开销,总计80M)。在游戏内的日常行为由于同事对材质组织和资源管理方面进行了优化,基本可以保持稳定在150M左右。当然,目前还需要进行一些收尾的工作,因为实际运行中尚存在一些占用较多释放不及时的问题。

2009年3月15日星期日

改写了继承时代码复用的方式(2)

昨天完成了编码工作,最后调试没有完,今天过去仔细调试了一下,然后并入客户端看看效果。

出乎我意料之外的是,优化的效果比我预测的要好得多。原先我估算过,每个VM指令大概占10个字节左右,其中4个字节指令,4个字节的runtime。每行符号信息是8个字节,因为平均5个指令一行,所以算下来每个指令用不到2个自己的符号信息。(除了指令,runtime和符号信息可以在发布时去除)

而整个系统启动以后大约是900K条指令,这样指令占用的内存应不到11M。然而我修改了复用方式以后,指令数下降到不到300K以内。按照道理应该是节约600Kx10=6M的内存,但是节约的内存居然是12M,而实际上节省的内存则多达20M。

我想了一下,应该是函数头的开销不可小觑。函数头、局部变量、参数等平均算下来可能消耗多达近300个字节。继承不再复制代码以后,函数的数量锐减了20多K,这部分内存则总计6M。

除此之外,因为函数头有很多小内存片(我已经看这个不顺眼很久了,但是一直没有把它作为重点对象照顾),导致占用了很多额外的内存。削减了函数数量以后也让这部分的空间节省下来了,将近8M。

如此说来,剩下的8K左右的函数,占用的内存额外开销应该也有近3M,如果改善这部分数据使用内存的方式,至少应该可以节约一大半,也就是2M左右。目前看来仍然有进一步压缩的空间。

2009年3月14日星期六

改写了继承时代码复用的方式

原先在对象继承时,我采用的是复制一份代码,同时对引用对象变量、调用函数、获取字符串资源等操作进行了映射转换。

目前看来,因为复制可能会导致大量的重复内容(尤其是对CEGUI Window的映射基类代码很多),占用不少内存,对客户端来说这点很敏感。在前一段优化工作完成以后,我开始着手修改这方面的实现。

原先所有映射、合并的代码全部删除。新增继承时代码段的组织方式,并且增加处理函数绑定的功能(这点是driver处理较为奇特的一点,一个对象可以声明函数F的prototype,但是并不实现,此时调用代码如果被执行则会出错。如果将来派生对象实现了相应的函数F,则该对象对F函数的调用自动定位到F上)

同时,VM新增了一个“当前程序段组件"的概念,相当于C++在类调用基类时需要修改类的this指针(比如VC使用的的ECX)一样,如果调用函数,必须更新这个值。同时在访问对象变量、对象字符串资源时,需要根据这个值进行查找,这带来了一些性能上的损耗,不过相比之下,获得了内存上的节省更划算一些。(在服务器端,我更喜欢原先的方式,但是考虑兼顾两种实现数据结构实在太复杂了,只要放弃这点性能了)

ps. 昨天遇到一件很有意思的事情,我在调整代码时,编译出的汇编代码完全没有变化,只是switch-case跳转表偏移了8个字节,结果导致VM执行测试脚本的性能下降一半。具体原因我尚不详,可能和cache命中率有一定关系。当然,根据结果决定是否调整意义不大,因为随便修改一下其他地方的代码都可能会影响结果。这可能导致一个很奇特的情况:有时候你明明将代码优化的更好了,但是实测效果反而更差。如果你不理会这个测试结果,继续修改其他地方,某一天你就会发现你的优化发挥了作用... 相比之下,GCC编译出来的结果要稳定一些,不过这也可能和CPU不同有关系。

更新 & 混乱

昨天下午将最新的driver提交上去了。在这个版本里面,我对driver的行为作了两个重大的修改:

1. 不再支持直接对string、buffer内部数据的修改,而是通过外部函数修改。
比如:
string str = "abc";
str[0] = 'x';
这个将不再支持,修改为:
put_sub_data(&str, 0, 'x')
2. 将实数从double修改为float

第二个修改还好,目前没有发现什么问题。第一个我原以为受到影响的代码应该很少,毕竟直接修改字符串内部数据的需求应该很少才对,相应的修改一下即可。然而,提交上去以后,QA手工又搜索出几处。今天一早过来,QA又在测试中发现几处... 看来,这种修改引起的混乱远比我想象的要大。

针对于driver行为的变化,需要有更好的测试手段才行。

2009年3月13日星期五

火大了

今天正在折腾driver新一轮的优化,对某些指令在进行改写,试图以此减少最终产生的指令数量。

快走时,外网服务器的性能检查结果出来了,我去看了看,一起讨论了一下。无意中,在检索源文件的时候,突然发现一个让我非常光火的事情:

我们原来有 一组脚本文件,用来让策划编写代码定制功能。后来因为有被滥用的趋势(比如脚本间互相调用,单一脚本太长),所以限制了脚本不能互相调用,单一脚本不能超过30行。因为有很多重复的公式,所以我们将它们抽取出来形成公式文件,允许通过脚本调用这些公式,以增强脚本的能力。

但是没想到现在公式则被滥用的厉害,公式内容极为复杂,完全替代了之前脚本的功能,早已不再是公式。这种开发人员为了达到目的而有意无意的不择手段钻空子的行为,实在是让我光火的厉害。

我想,除了一方面增强这些限制以外,还需要对策划编写的脚本进行code review。然而,不仅如此:还必须培养相关开发人员负责的态度,用正确的方法解决问题的意识,否则,他们终究还是能够找到破绽。

2009年3月11日星期三

万物皆有因、众人全网游

我在编译库的时候有一个诡秘的bug,如果在工程的根目录下make,那么tinyxml的生成的库就不对;但是如果到tinyxml下面去make就正确。

我一直没有去解决这个问题,只是每次手工进tinyxml再make一次。

最近连续修改driver的头文件,每次都需要重新build tinyxml(因为wrapper使用了头文件,很不方便。检查了一下原因,看了半天竟然没有发现问题,make出来的库是正确的,但是copy以后就不正确了。在Linux、MacOS下面都有这个问题,所以这显然是我的一个弱智错误造成的。

后来我突然想起来,应该在磁盘上搜索一下有没有其他tinyxml的生成库,也许就是从这里copy出来的。一查就发现了原因:我以前增加的csoap使用的也是tinyxml的makefile,忘记修改工程名。因为那个库从来没有在Linux下make并且测试过(在Win32下面测试过,因为没有进入项目,就没有去Linux下测试)。make的时候,进入csoap将生成和tingxml同名的库,然后copy就导致了错误的覆盖。

真是万事皆有因啊!

另外一个挺神奇的事情:陈拓琳的MM也在玩问道。她以前从来不玩网游,所以基本概念全都不知,对游戏也没有什么兴趣,看上去也很不敏感,只是勉强玩玩。我一直宣称她不是我们的目标用户,属于被忽视的对象。但是今天我们下班准备回家的时候,陈拓琳的MM说要刷怪,要等等再去接他...

哎,看来无人不可网游。

2009年3月10日星期二

临车开了,才发现自己没买票

早晨本来想晚点起床,好好休息一下,结果8点多楼上叮叮咣咣的装修,实在躺不下去了,干脆爬起来上班去了。

昨天更新了driver,本来以为今天可以顺利发布出去。没想到,QA在验收的时候发现一个BUG。虽然在告诉我BUG的一霎那我就知道了错误所在的位置,但是因为正在开发,没法更新,只好先让他们绕过。

正所谓:临车开了,才发现自己没买票。

ps. 中午一个做证券的朋友来拜访,谈到网游企业股票的投资价值问题。我认为现在行业很不错,还有很大的发展空间,要是把中国网游公司的股票打包做个基金,买点还是很不错的。

草率易出错,编码需谨慎

今天等SVN上的版本管理方法稳定下来以后,我开始提交这更新后的driver。

因为改动很多,所以我针对Debug/Release版本都仔细测试了一下,登录进入游戏,看上去很好,没有什么问题,于是放心提交了。

晚上突然出了点意外,有一两个玩家运行客户端以后卡在一开始的启动界面就不动了 - 这个发出去的客户端是上周五晚上build的,尚没有包含我更新后的driver。一个同事检查了半天,最终确认OGRE、CEGUI均无问题,一直到driver启动也没有意外。

于是我在本地build了一个版本,传给对方,没想到一过去就崩溃了。很纳闷,回来看了看,发现版本组织有点变化,原本build出来的平台lib & dll会自动拷贝到发布目录下,现在居然没有这一步了。一问,需要手工拷贝 !@(#*!&@#*(!@ 也就是说,我今天提交的driver其实根本没有经过测试,只是编译通过了而已,生成的dll没有放到发布的目录,所以没有被使用。对工程做出这样的变化没有周知真是添乱啊!

好一番折腾,排除了几个driver的bug以后总算build出来了稳定的客户端。发给对方,没有改善,但是我发现启动以后即使什么都不做,机器的CPU占用率也特别高。回到我本机一试验,果然,一启动就是50%,同时加载脚本的话就到了100%,而我是双核的机器,说明一开始就耗尽了一个核的资源。既然在本地能够重现,问题就好办了。

诊断的结果让我有些无语,原来是启动界面处理消息在PeekMessage以后没有Sleep。最早的代码当然有,但是上周合并版本时,程序员遇到了一些问题,点击退出无效,于是对代码进行了一些调整。显然,他们没有找到真正的原因,只是草率在收到点击退出时调用exit(),进行了很不优雅的退出;另外,他们还随意修改了一处和退出无关的代码,导致CPU占用一个核的100%。而外网那些只有一个核的玩家因此就倒了霉,启动时大部分时间都用来做死循环了。

所以说,处理问题不可草率,要想清楚原因,做出合理的解释,才能针对性的设计解决方案。倘若到处对可疑代码打补丁,那不是排除问题,而是埋地雷。

ps. 既然单独开了一个线程取消息,为什么不用GetMessage而是PeekMessage?想法真诡秘啊!

2009年3月8日星期日

gettimeofday

昨天走之前测试的时候瞥了一眼,突然发现Linux(CentOS)下面调用外部函数的时间居然比调用脚本函数的时间还要多得多,这是不应该的。当时已经晚了,我没有继续测试。

回到家里我仔细想了想,外部函数和内部函数不同之处在于每次返回的时候都会检查脚本线程使用的tick是否已经超过了预期,如果是则需要进行调度。莫非是取tick的函数执行的太慢?

Windows下面取tick用的是GetTickCount(),虽然这个精度很低,但是速度的确很快。Mac/Linux下面用的都是gettimeofday,Mac下表现的很正常,只是Linux不正常。考虑到这个函数的精度很高(见前文,现在可以真正有微秒级的精度),需要硬件支持,可能问题就出在这里。

今天中午我到公司一测,果然:在Linux下面执行gettimeofday居然用了0.9us,在这台机器上执行一条VM指令也不过才0.02us,这个调用果然有点问题。

显然应该不是所有版本的Linux都会如此,应该和系统内核所配备的驱动程序有关。当初在给问道更换服务器的时候,曾经出现过新装的机器承载人数比老的机器低40%以上的情况。当时我实测了一下环境,CPU很快、内存吞吐很快,服务器耗在网络上的资源也很少,而剩下的脚本逻辑VM基本都不会和系统调用打交道(除了网络,无任何IO),后来将服务器的Linux版本从RedHat4降级到RedHat3解决了问题,说明和驱动相关的可能很大。目前看来,时间函数可能是原因之一。

2009年3月7日星期六

高精度的计时器

这几天在优化的时候,GetTickCount()的变化频率太低,一个周期超过了10ms,不利于统计,于是想增加高精度的计时器。

在Windows下面这个很容易做,不用GetTickCount()。而是封装使用QueryPerformanceCounter/Frequency()即可。但是在Unix类的系统下则遇到了问题。C/C++标准库中是没有高精度计时器,POSIX也没有。检索了一下Linux下的资料,看样子要对内核做一个小patch才能够使用,很不方便(我并不希望每换一个机器都patch一下)

于是我打算采用古老的RDTSC来达到这个目的,虽然这个在SPEED STEP等情况会有问题,但是对测试来说是足够了。

RDTSC取的值很精确,但是每秒钟它变化频率则是一个大麻烦。从CPUID中取得的主频不可靠,INTEL的手册中给出的方法很罗嗦,根据XE的值还有不同的分支,而且我怀疑这个方法也不太靠得住。因为Intel的网站上有人给出了这样的方法:即最古老的 - 小睡片刻,然后根据这段时间差的计数变化情况计算。

既然如此,我还是用这个方法好了,算是比较可靠的一种:


#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <errno.h>

/* Return ms counter */
Vm_Tick_T _vm_getOSTick()
{
/* BSD4.3 get time */
struct timeval tv;
struct timezone tz;

gettimeofday(&tv, &tz);
return (Vm_Tick_T) (tv.tv_sec * 1000 + tv.tv_usec / 1000);
}

#ifdef __APPLE__
#define DO_RDTSC \
__asm { push eax } \
__asm { push edx } \
__asm { rdtsc } \
__asm { mov r_eax, eax } \
__asm { mov r_edx, edx } \
__asm { pop edx } \
__asm { pop eax }
#else
#define DO_RDTSC \
__asm__ __volatile__ ("pushl %eax\n"); \
__asm__ __volatile__ ("pushl %edx\n"); \
__asm__ __volatile__ ("rdtsc\n"); \
__asm__ __volatile__ ("movl %%eax, %0\n" : "=m" (r_eax) : ); \
__asm__ __volatile__ ("movl %%edx, %0\n" : "=m" (r_edx) : ); \
__asm__ __volatile__ ("popl %edx\n"); \
__asm__ __volatile__ ("popl %eax\n");
#endif

/* Return us counter */
Vm_Freq_T _vm_getOSUsCounter()
{
#ifdef IA_RDTSC
/* Use RDTSC */
static unsigned long long timeFreq = 0;
union
{
unsigned long r_eax_edx[2];
unsigned long long timestamp;
} u_now, u_prev;
static volatile unsigned long r_eax, r_edx;

if (timeFreq == 0)
{
/* First invoking, stat for frequency */
Vm_Tick_T c_now, c_prev;

/* Wait clock changed */
c_prev = _vm_getOSTick();
while (c_prev == _vm_getOSTick());
c_prev = _vm_getOSTick();

/* RDTSC - save time stamp counter */
DO_RDTSC;

/* Wait clock changed */
u_prev.r_eax_edx[0] = r_eax;
u_prev.r_eax_edx[1] = r_edx;

/* At least 10 ticks passed */
while (c_prev + 10 > (c_now = _vm_getOSTick()));

/* RDTSC - get new time stamp counter after sleep */
DO_RDTSC;

/* Get frequency per second */
u_now.r_eax_edx[0] = r_eax;
u_now.r_eax_edx[1] = r_edx;
timeFreq = (u_now.timestamp - u_prev.timestamp) * 1000 / (c_now - c_prev);
}

/* Get current counter */
DO_RDTSC;
u_now.r_eax_edx[0] = r_eax;
u_now.r_eax_edx[1] = r_edx;

/* Convert to us, return in 32bits */
return (Vm_Freq_T) (u_now.timestamp * 1000000 / timeFreq);
#else
/* BSD4.3 get time */
struct timeval tv;
struct timezone tz;

gettimeofday(&tv, &tz);
return (Vm_Freq_T) tv.tv_sec * 1000000 + tv.tv_usec;
#endif
}




其中Vm_Tick_T & Vm_Freq_T都是32位数即可。

如果使用RDTSC,则在计算时间时,第一次调用会测量10ms,从而得出频率,以后则依据频率计算。

不过,让我吐血的事情在后面:

当我做实验的时候,意外的发现其实gettimeofday返回的很准,相当的精准,准确到了微妙级别(最早我在开发driver即2001年的时候,使用的系统实现这个方法很不准,误差在10ms这个级别。几年过去了,下面的平台早已有了质的飞跃...)。也就是说,我白忙活了半天,算了,留着这段代码在driver里面做纪念吧!也许将来我会需要一个ns级别的计时器,不过那时应该依赖于微妙而不是毫秒计时器来计算Frequency。

微小的性能优化

昨天睡觉的时候想起一个事情可以做做,早晨过来就实验了一下。

LPC支持用文件名直接访问对象,也就是说对象名就是文件名。比如“/daemons/filed.c”,即是文件名,也是这个文件载入后的对象名。因此其他模块可以直接进行类似“/daemons/filed.c”->do_somthing()这样的调用。

我之前的实现针对这点有一定的优化:每个字符串结构带了一个16位的hash值,当字符串做过hash运算以后会保存这个hash值,将来不需要再重复一次。但是,查找对象时,最后比较是否匹配还是要进行整个字符串的比较:STRCMP。

我尝试将对象的名字放入共享字符串池,当进行通过字符串查找对象时,先从共享池中取得字符串,然后直接判断指针是否相等就可以判断名字是否相同了。在实际编码的时候,还是遇到了一些小问题:比如调用者如果没有按照标准的路径名书写对象名,比如“daemons/filed.c”,这样也可以查找到对象才可以。显然,不可能从共享池中取得这个字符串 - 即是取得了,也无法用于判断。因此最终我实现的逻辑如下:

IF string in shared strings pool THEN
DO lookup object
RETURN when found

DO regular string name
IF string name isn't changed
RETURN null

DO raise warning - 这是为了提醒调用者,注意传入正确的字符串,免得每次都要进行规范化操作

DO find string in shared strings pool
IF not found THEN
RETURN null - 字符串名字不在共享池中,自然不可能找到对象

DO lookup object
RETURN result - 不管结果如何,都需要返回了

这样,如果字符串名字本身是标准的,并且在共享池中(代码中的常量都是在字符串的共享池中的),那么将只需要进行整数操作就可以进行hash检索,无需比较字符串的内容了。

实测效率从一次调用耗时0.42us下降到了0.3us。考虑本身循环调用、进出入函数的开销就需要0.18us,相当于检索耗时从0.24us下降到了0.12us,提高了一倍。

不过我做的另一个实验没有达到预期的效果:VM在执行指令时,需要记录一些“当前”信息,比如ip、sp、thisObject等,当进入函数时需要保存这些信息,即将它们记录到context中。原先是逐个变量赋值的,类似:

context->ip = vm_ip;
context->sp = vm_sp;
context->thisObject = vm_thisObject;
...
// 一共5个成员

我尝试将这些信息归入到一个结构中,然后通过结构赋值(编译后即为repz movs)保存,结果效率反而下降了一些。对CPU来说,如果数据不多,它更喜欢“内存->寄存器->内存”这样几个连续的操作。可以并行处理,优于repz movs,因为它还需要准备esi、edi、ecx。

2009年3月5日星期四

侥幸

今天和蓝港的人交流了一下,本日发号行动只是到媒体,派送到玩家还需要一段时间,所以尚不会出现大规模登录的问题,比较侥幸。一整天,服务器大半时间都在维护,还好这时候没人添乱。

下午在实验室里面果然发现了几个driver的bug,一个是我编译库用的mysql的版本和其他人提交的mysqlclient库不一致,另一个则是driver优化后的逻辑错误,解决这几个问题花的时间不算多,还算是比较顺利,就是提交的时候有点麻烦。

面临考验

下午的时候,QA报过来一个问题,Windows下的DBA启动时driver会崩溃。这个问题还没来得及检查,程序又报过来一个问题,Mysql连接数似乎只能有32个了。

当时我手里的工作正进行到一半,等做完就到了体育锻炼时间,先去打球,8:00钟才回到公司。

在我入手排查bug的时候,突然收到QA的消息:蓝港明天就要放号进行新一轮测试,这个讯息已经发布到媒体了。我感觉很不妙,目前driver正处于更新期间,尚没有稳定下来,眼前已经暴露的问题还好办,如果等到人数上来的时候新暴露问题可就很掉链子了。我询问了一下是否能退回之前运行了一周的版本,被告知不可,因为最新这个版本调整了经验等公式,如果退回之前的版本,就意味着新进来的玩家马上就会面临策划数值的变动,同时之前的版本缺少很多系统,和宣传不符。

总的来说就是一句话:后退是火坑,前进是沼泽。

这让我想起了奥美运营的《孔雀王》,在公测前一天还进行了海量更新,结果惨淡收场,尸体一直铺到苏门答腊。

晚上抓紧定位并解决了DBA启动driver会崩溃的问题,但是我并没有更新,因为我手头的driver又进行了新的优化工作,其中不仅逻辑发生了变化,同时还影响了头文件,这意味着很多库需要重新编译链接。虽然我可以取出历史版本,针对性的修改这个BUG,但是忙中易出错,所以就不修正这个BUG了,毕竟这个BUG理论上只要启动过程OK,就不至于出现危险的结果。倘若真的没顶住... 那只有兵来将挡、水来土掩、见招拆招了。

目前问鼎这个版本服务器端、客户端都在优化进行中,driver不稳定,这可能导致服务器端出现崩溃的可能;服务器端性能目前令人不满意,正在调试优化 中,而新增的任何手段都意味着存在毛病的可能;策划刚刚进行了一轮设定上的改动和更新,需要观察效果;排队机制尚未实现,如果同时涌入人较多可能会在登录 环节遭到非常不好的体验 - 总之,明天将发布一个未经考验并且肯定有问题的版本。

可以预见的是,明天很可能是一个糟糕的局面。担心无用,懊悔也是无用,只能先走一步看一步,将最终的公测做好才是正道。

2009年3月3日星期二

无题

昨天晚上下班的时候,突然感觉颈椎有些痛,吓了我一跳,这么多年来从未有过这样的情况。本来以为睡一觉即可恢复,但是没想到早起还是很痛,也许是昨天休息的很不好,也许是需要一段时间才能恢复。

我想是前一段时间坐姿有些问题,因为连续10天都在电脑前面工作,很少离座,而电脑在我左前方,要侧着头,很不舒服,时间久了就容易出问题。我一早过去,首先就是把显示器板正,但是因为桌面的东西有点多(还有一个笔记本),怎么摆都有些别扭。

本来我打算今天完成VM指令长度精简的工作,到可以提交的程度。没想到今天会议有点多,用掉了4个小时,结果晚上只完成了driver的自测,放到工程中还是有问题,没来得及全部排查,看来只能明天继续了。

我试验了一下压缩指令长度以后的效果,速度居然比以前版本的driver慢了50%,让我有些纳闷。检查了一下,发现我Release版本的优化选项居然没开(应该是以前检查bug时关闭了),打开了O2以后,速度恢复正常了,但是仍然比原先的driver略慢10%。这有一半在我的意料之中,因为指令精简以后,有一些信息不能再简单的获得了,需要通过指针多索引一次。不过我并不打算到此结束,打开调试看了一下汇编代码,发现几个问题:

1. VC O2优化的程度不够
我将值保存进入栈中的局部变量,然后在用这个局部变量作为参数调用。这个局部变量其实应该被优化掉,但是目标代码仍然进行了一个多此一举的mov,也许开更高的优化选项可以解决这个问题,但是我不想冒险,毕竟O2才是久经考验的。我调整了一下C语言代码的组织,去掉了这个局部变量就解决了问题。

2. switch-case不是我期望的工作方式
有一段代码类似如下:
switch (parameters)
{
default:
VM_ASSERT(parameters == 0);
...
break;

case 1:
...
break;

case 2:
...
break;
}
其中parameters只可能是0、1、2。目标实际上是先判断2,然后dec,判断1,最后跳转到default分支。这不是我想要的,因为大部分VM指令都是无参数的,所以这里进行了一下调整:
if (parameters == 0)
{
...
} else
if (parameters == 1)
{
...
} else
{
...
}
同时,我注意到这段代码之前有一段分支,就是判断是否有从另一处取参数(如果指令参数较长,不能保存在指令内,需要另取),如果是则另取参数,然后再执行到这个分支。我考虑了一下,将以上代码复制了一份。这样如果另取参数时,可以减少一次判断(因为不可能有参数0的情况),同时,如果只有一个参数则不需要取第二个参数,节约了一条指令。

进行以上修改以后,执行VM指令的速度提高了将近30%(当然,这是针对简单指令而言的,如果是复杂指令,提高的不会太多),全面超越了之前的版本。

2009年3月2日星期一

对VM的进一步优化

今天召集了几个人组成了包括我在内的8人优化小组,专门对问鼎进行优化。三个服务器端、三个客户端、一个策划、一个美术,可以说是公司目前最佳技术力量,争取在1-2个月之内达成优化目标。

因为人力相对充裕,所以我考虑对VM做进一步的优化。本来我并不打算做这部分工作,因为性价比不高,要编写很多代码,而获得的收益有限。但是在小组其他人可以解决发现的主要问题以后,这些次要问题也将上升为主要问题了,所以不如早点动手,让driver能够经受更长时间的考验。

目前我在将driver 12字节长的指令修改为4字节的 - 当初为了省事,我设计每个指令使用两个IntR参数(也就是64位的driver这两个参数占据16个字节,单条指令将有20个字节),这当然很奢侈。不过事实上,在服务器端内存充裕的情况下,这算不了什么,区区20M空间无足轻重,因为cache失效而浪费的CPU资源也有限的很。但是在客户端则有所不同,每节约1M都是令人振奋的,毕竟很多玩家的机器还只有512M内存。

2000以后的我设计思路和90年代初有了很大的不同。在640K内存下编码的时候,每一个比特都要精打细算,思考如何做能够更加节省。而现在,我更多的考虑是如何简洁、可靠、容易维护,效率不再是优先考虑的因素了。毕竟内存已经由不到1M飞涨到了128M+,就连嵌入式系统上也有超过16M的内存空间了。我想,等到用户桌面机器普遍16G内存的时代来临以后,程序设计可以更加轻松。处理逻辑方面几乎不需要考虑内存的问题,主要的内存是用于保存多媒体数据,逻辑占用的数据只是凤毛麟角。就现在而言,真正影响客户端资源占用的还是贴图,节省一张被浪费的贴图,就足以相当于我一天的工作了。

2009年3月1日星期日

噪音与宁静

前一段时间连续十天保持了每天14小时左右的工作强度,今天感觉有点疲惫,早起的时候懒得动弹,结果早餐也没吃 - 在我印象中,在厦门的时候是极少发生类似情况的。

大概是随着年纪的增长,精力已经不再像以前那样充沛了,不吃不喝不睡似乎也没多大的关系。编码本身似乎还有充能的效果(这听起来有点像永动机)。不过,我认为这并不是主要原因。毕竟每天我还能在12点多休息,可以睡到7:30,从休息时间来说应该是足够的。问题是:我睡眠的质量很差。

我一贯以来的神经衰弱,入睡很困难,而醒来则容易(这在几年以前其实算是好事);另一方面,我对声音非常敏感,很小的声音我都能听的很清楚(令人沮丧的是我节奏感很差,标准的音盲);第三点,也是最重要的一点,就是附近实在太吵了,噪音很多。

从 5:30开始,附近的军营就出操了,口号声很响亮,不过这个还好,我倒不觉得很厌烦。而附近路上渐渐的有了车,时不时还会鸣笛,间或还能听到有人很大声的说话。唉,很多国人不以制造噪音为罪恶,确切的说,国人更享受嘈杂。每当这时候,我格外怀念硅谷,那里随处可以找到符合我对声音要求的小区。

在厦门快6年了,期间换过很多地方:禾祥苑、鹭江新城、永升花园、珍珠湾、明发海景苑、云海山庄、还有就是现在的瑞景公园。其中,禾祥苑最安静,永升花园尚好,珍珠湾就比较吵,目前住的瑞景次之,云海山庄就比较吵了,而明发噪音则更大,最让人无法忍受的就是鹭江新城,每天晚上半夜三更还有一小撮人喝酒、大声喧哗。

想找一个离公司不远,而又称得上宁静的地方,真是很难。

关于垃圾回收

嗯,叫做垃圾回收还是叫做垃圾收集,这是一个问题,我在用中文搜索相关资料的时候就遇到了这个别名带来的麻烦。当然,如果叫做garbage collect就没这个问题了,可是那对检索中文资料又带来了困难。

不过事实证明,检索中文资料不是那么重要,因为几乎所有讲具体算法的书,都要付费下载...

我原本一直对垃圾回收有一定的偏见,因为它的不确定性,让我不能准确的使用内存和CPU的资源,也就是会带来那种在掌控之外的感觉,很不好。所以我比较喜欢用引用计数,但是我现在必须改变我的看法,因为引用计数的效率有点低 - 我之前居然一直没有关注到这一点,真是有些不可饶恕。

目前driver使用的值(value)和映射(mapping)的节点分别只有12、8个字节。如果为他们使用一个32位的引用计数字段,那就是4个字节,浪费了33%、50%。事实上,当然还不止这些,因为这些小内存片使用了引用、释放后,大部分情况必须作为独立分配的内存块而存在,否则释放会相当繁琐。既然是独立的内存块,那么指向他们需要一个指针,又是4个字节,管理这些小片内存,往往也需要一点资源 - 很好,一般还是4个字节(虽然有一些精巧的技术可以压缩这些空间,但是会带来很大的使用不便)。这么算下来,有效数据是12、8个字节,而额外的开销却是12个字节,浪费超过了100%。

同时,引用计数一个糟糕的情况就是,不能简单的使用内存拷贝。必须在复制指针的时候进行计数累加,在废弃指针的时候进行计数递减 - 糟糕的是这里还需要判断结果,这是一条判断跳转语句,这一切都使得执行过程显著开销增大。

对于过小的数据单元(类似上面提到的value & mapping node)采用引用计数并不是一个好主意。遗憾的是当年我并没有认真的考虑到这一点,现在真是让我捶胸顿足,后悔的很。

目前driver有一个用简单回收算法(mark-sweep)实现的回收器。但是我却不敢使用它 - 一:效率不高;二:算法是非增量的,导致系统会突然卡一下。

也许我应针对具体应用做具体的改进。