Home » 未分类 » Scout性能优化实战——Point对象专项整理

Scout性能优化实战——Point对象专项整理

Scout性能优化系列——Point对象专项整理

文章首发于 游戏研发笔记

image001 image003

话题开始之前,我们先来看两组数据,图1是《西游归来》的垃圾回收数据,图2是《剑雨江湖》的垃圾回收数据。下表是对两份数据进行统计对比。

游戏名称 (垃圾GC)平均耗时 GC耗时峰值 超越红线GC频率
西游归来 3 ms 458 ms 5 /分钟
剑雨江湖 3 ms 39 ms 1次 /分钟

从游戏体验来看,明显可以看出,《西游》出现卡顿比较频繁,而《剑雨》相对来说要表现的更加流畅。西游GC耗时频繁出现峰值,并且峰值高达422ms。我们不禁要问,为什么GC要这么久?(注意:《西游》处于研发中,当前表现不代表最终品质

这里,不得不简单补充一下Flash Player内存管理相关的知识。如果您是已经很了解其内幕,可以跳过。

image005

 image006 黑色对象已标记,不再位于队列中。
 image008 灰色对象位于队列中,还未被标记。
 image010 白色对象既未标记也不在队列中。

在Flash Player内部内存管理的方法,Flash Player结合使用延迟的引用计数和保守的标记并清除(mark-and-sweep)方法。从上面的三色标记算法图示,可以得出一个简单粗暴的结论:也就是相同时间内,新建的对象越多,也就意味着GC器的Work Queue(尤其是灰色和白色)会越长, GC器就会花费越多的时间去逐个扫描并进行标记,然后根据内存需求的紧迫程度进行清除的时间也就越长。

如果想要进一步学习,Flash Player内部内存管理点击这里《Flash Player和Adobe AIR垃圾收集内幕》

接下来,我们回到案例本身。为什么《西游》GC时间的峰值,会比《剑雨》的GC时间峰值高出10多倍呢?我们在Scout中,定位到GC峰值所在帧,可以得到以下统计数据。也就不难理解,为什么《西游》GC耗时会高出那么多。所以,要想获得比较平稳的性能,避免GC时间过长导致游戏频繁出现卡顿,必须做好内存管理。尤其是要做好对象的缓存和复用。尽可能的减少对象被重新分配。

游戏名称 GC耗时峰值 重新分配对象数量 重新分配内存
西游归来 458 ms 15846 9754 KB
剑雨江湖 39 ms 4283 个 410 KB
比率 11倍 3倍 23倍

image012image014

接下来,我们将进一步实战,学习如何通过Scout发现哪些对象被重复创建然后又被重新分配,这样来来回回的在那里“捣乱”。这里我们以Point对象内存泛滥专题整治为例,让我们一起看看,那些代码在不断new Point()然后又把这个对象随便扔给垃圾回收器,造成垃圾回收器超负荷工作。选中一段区间,建议从第1帧开始选取,可以在内存分配窗口看到Point对象被创建数量以及对应的代码位置。

image016

我们把排名靠前的几处代码揪出来,好好审判一番:

一、bresenhamNodes() 这个方法是A星寻路中的一段。由于玩家在地图频繁移动和更改路径,这里会创建许多的Point来表示路径。

image018

解决方案A 使用Point对象池,每次要用,从池子里面pop()一个,不用了再push()回Point池。缺点就是什么不用了,这个需要做好管理,管理不好,容易造成内存泄漏。

解决方案B(推荐):不用Point数组来表示路径,而是改用数值来表示。改之前的结构是这样的[p1,p2,p3,p4……],改之后的结构变成这样[p1x,p1y , p2x,p2y , p3x,p3y ,p4x,p4y……]。能用数值类型来解决参数传递的问题,就不要用Point来解决。另外要给推荐理由,数值类型是在栈内存当中保存的,而Point对象是保存在堆内存的。

二、get curP() ,BaseLife.get position()等这种get型属性,会给程序带来一定偷懒的机会,但是却会很容易造成Point对象的滥用。

image022

image020

解决方案:

  1. 直接调用x ,y 属性 ,最好不要对外提供这种类型的接口(最安全的)
  2. 用一个私有的Point来保存x,y,而不是每次访问属性都会新建。如果开发需要新建Point自行Clone()就好了。

三、以movePart()为代表的纯粹数值计算。这里Point仅仅是用来辅助计算。而且由于每帧里都会调用到,如果场景中有100个MagicV,那么每一帧可能就会需要分配200-400个Point来,1秒种(60帧/秒)下来,就会需要分配1200-2400个Point,同时也意味着会有1200到2400个Point对象需要回收。真心心疼GC回收,干活好累。看上去性能是一点一滴的省下来的,但是被某种特殊条件触发,这就会是一笔很大开销。

image024

解决方案:

A、一个静态辅助对象Point足以。不管有多少个MagicV,每帧被调用多少次,此生,只需要生成一个辅助Point就可以搞定,回收器可以下岗了。

B、改用 值类型,两个方法合成一个方法。性能是最好。当然可能会让代码不好看。

四、最后,我们再抓一个“坏蛋”典型。这是一个工具类型的Point数值的转化。输入一个Point,然后得到一个新的Point。绝大多数时候,我们只是对输出的Point的X、Y值进行计算,然后就把Point给扔掉了。在BaseLifeV中有个方法updateAlpha(),就是每帧里面,会把玩家的像素位置转换成网格坐标,然后从配置中判断当前位置是否需要将玩家设置为半透明。这里的浪费的Point数量,和这个方法被调用的次数成正比。

image026

解决方法:

这个是比较取巧的方法。在方法参数列表中,新增一个参数,允许传入一个已经创建的Point,来接收结果。从而避免新建Point。

image028

五、本来已经到了最后总结,但是通过Scout发现还有一处比较坑的,这个就是FlashRuntime自带的distance()方法。计算两点之间的距离,如果调用Point.distance方法,会生成新的点,而且计算速度也没有自己写的方法快。下面是测试代码:

image030image032

image034

测试结果:使用Flash自带的类计算两点之间的距离会10000个点需要58ms , 而使用下面自己写的代码,计算10000次距离,只需要耗时3ms 。所以,不能太迷信,Flash Player原生的方法也不一定就是最好的。

总结:

通过Scout发现频繁new创建的对象,然后能不创建的就不要创建,能用值类型计算的,就不要用Point。一点一点的把性能给省下来。这里Point这是块砖,希望能够举一反三,在Array ,String , MethodClosure , Function 以及一些其他的自定义类对象等等,方面,做好缓存,避免给GC器造成超负荷。

最后,纠正一下前面的数据。《西游归来》数据是Debug版本,而《剑雨》数据是采集子Release版本。真实数据差距可能没有那么大。但也不会好太多

    分享到: