Monday, March 13, 2006

J2ME 游戏优化探密(二)

哪些地方需要优化——九一原则

在性能较低的游戏中,90% 的时间被花费在运行 10% 的代码上。这 10% 的代码正是我们需要集中所有努力优化的地方。我们用一个描述器(profiler)来找出这 10% 的代码。要打开 J2ME Wireless Toolkit 中的 Profiler Utility,可以在 Edit 菜单中选择 Preference 项,打开 Preference 窗口,选择 Monitoring 标签,勾选标有“Enable Profiling”的复选框,然后点击“OK”按钮。没有任何事情发生?没关系——我们需要在模拟器中运行我们的程序,然后退出,这样 Profiler 窗口才会出现。现在就试试看吧!

Figure 1. 演示如何打开 Profiler Utility


我的模拟器(运行在 Windows XP 下,Intel P4 2.4GHz CPU)报告 100 次循环花费了 6,407ms,即 6 秒半种,每帧 61-63ms。在硬件(Motorala i85s)上,它运行慢许多。没帧的时间在 500ms 左右,而整个程序跑了 52460ms。在这篇文章种,我们将尝试改进这一现象。

当你退出程序时,描述器窗口将弹出来这样你就能看到一个类似于文件夹浏览器的东西,左边面板有一棵树,它分级显示了方法之间的关系。每个文加夹都是一个方法,打开一个方法的文件夹显示所有该方法调用的其它方法。在树中选择一个方法,将在右边窗口中显示该方法的描述信息以及所有其它被它调用的方法。注意每个元素附近显示有一个百分数。这个是该方法运行花费的时间占总运行时间的百分比。我们需要浏览这棵树,找出时间到花到哪里去了,然后尽可能地优化那些占有最高百分比的方法。

Figure 2. Profiler Utility 调用表单


关于描述器,有些地方需要注意。首先,你所得到的百分比可能和我的会不同,但是方法类似——总是追踪那些最大的数字。我每次运行程序,这些数字都不同。在做测试时,请尽可能地保持环境统一,你可能需要关掉所有的后台应用程序,比如电子邮件客户端等等。同时,在使用描述器之前也不要采取任何代码保护措施(如某些让源代码变混乱以防被反编译的工具),否则你的方法会被莫明奇妙的命名为“b”、“a”或者“ff”之类。最后,描述器本身对性能没有任何帮助,不管你模拟什么设备。硬件本身是完全不同的东西。

打开具有最高百分率的文件夹,我们发现 66.8% 的运行时间被名为 com.sun.kvem.midp.lcdui.EmulEventHandler$EventLoop.run 的方法消耗,这对我们帮助不大。继续向下挖掘一两层具有类似奇怪名称的方法文件夹,你将追踪到大的百分比方法 serviceRepaints(),然后最终到我们的 OCanvas.paint() 方法。另外 30% 的时间被它消耗。这两个方法都存在于游戏的主循环终,这并不奇怪——我们不会花费时间去优化 MIDlet 类中的代码,同时你也不会去为优化游戏主循环之外的代码费心。只优化那些反复运行的部分。

我们例子中这些百分数的分配和真实游戏的情况不会差太远。你会发现,一个真实视频游戏的绝大部分运行时间的大部分都被花费在 paint() 方法上。和非图像处理的部分比起来,图像处理的常规操作会花费非常多的时间。遗憾的是,图像处理的常规操作早已封装在 J2ME API 之下,在这一点上,我们没有多少余地用来改善其性能。我们所能做的是,聪明地决定我们用它们中的哪一个,以及如何使用它们。

高端优化 VS. 低端优化

本文后面将讨论有关低端优化技术。你将发现它们很容易应用到已存在的代码中,虽然会降低代码的可读性,但是会带来性能的提升。在你使用这些技术之前,最好先改善你代码的设计以及规则(algorithms),也就是高端优化。

Michael Abrash,id software 力作 Quake 开发者之一,曾经写到,“最好的优化工具,在你的双耳之间”。一个问题总有不止一个解决办法,如果在行动之前花些时间来考虑正确的方法,则可以达到事半功倍的的效果。使用正确的(比如最快的)规则体系,对性能的帮助将远远大于用低端技术来改进二流的设计带来的效果。用低端技术你可能可以降低几个百分点,但是之前请从高端优化开始,多用你的脑子——它就在你的双耳之间。

那么,让我们来看看我们在 paint() 方法里面干了些什么。每次循环我们调用了 16 次 Graphics.drawString() 方法用来在屏幕上显示“n ms per frame”。我们并不知道 drawString 方法内部的工作原理,但是我们知道它被用了很多次,于是让我们尝试一下另一种途径。我们可以把字符串直接一次性画到一个 Image 对象中,然后将该 Image 绘制 16 次。
public void paint(Graphics g) {
g.setColor( COLOR_BG );
g.fillRect( 0, 0, getWidth(), getHeight() );
Font font = Font.getFont( Font.FACE_PROPORTIONAL,
Font.STYLE_BOLD | Font.STYLE_ITALIC,
Font.SIZE_SMALL );
String msMessage = frameTime + "ms per frame";
Image stringImage =
Image.createImage( font.stringWidth( msMessage ),
font.getBaselinePosition() );
Graphics imageGraphics = stringImage.getGraphics();
imageGraphics.setColor( COLOR_BG );
imageGraphics.fillRect( 0, 0, stringImage.getWidth(),
stringImage.getHeight() );
imageGraphics.setColor( COLOR_FG );
imageGraphics.setFont( font );
imageGraphics.drawString( msMessage, 0, 0,
Graphics.TOP | Graphics.LEFT );
for ( int i = 0 ; i < DRAW_COUNT ; i ++ ) {
g.drawImage( stringImage, getRandom( getWidth() ),
getRandom( getHeight() ),
Graphics.VCENTER | Graphics.HCENTER );
}
}

当我们运行这个版本的程序时,我们会发现在 paint() 方法上花费的时间的百分率变小了一点点。更深入的观察我们会发现 drawString 方法仅被调用了 101 次,而 drawImage() 方法做了大部分的动作,被调用了 1616 次。尽管如此我们还是有成效,程序跑得更快了,因为我们使用的图形调用更快。

你可能会注意到将字符串绘制到图像会影响显示,因为 J2ME 并不支持图像的透明处理,因此很多背景被覆盖。对于前面所提到的,优化会导致你对应用的需求重新进行评估,这便是一个很好的例子。如果你真的需要文字的重叠,那么你可能就不得不去处理较慢的运行速度。

这端代码可能会稍好一些,但是仍然有很多优化的空间。接下来我们一起看看我们的第一个低端优化技术。

(一) (二) (三) (四) (五)