在游戏引擎中的JS性能优化

3,857 阅读5分钟

优化的适用范围

最近在做游戏引擎性能优化,关于js执行性能有些内容拿来这里分享。

首先需要明确两点:

第一,本文讨论的js性能问题,都是在大量执行的情况下才暴露出来的。一般来说,60fps的游戏,如果每帧需要执行2000次以上,那么就可以考虑本文的优化思路了。如果执行频次没有达到以上量级,性能并不会有明显提升。

第二,得到的这些性能红利在某些情况下需要牺牲代码结构与可读性,考虑在实际项目中是否值得。很多时候,需要牺牲一点点性能来使你的代码更容易维护。切忌对项目过度优化。

哪些点可以优化

以下几点,已经证明在大量执行的情况下,会对项目性能产生影响。

函数调用

设计模式和重构原则经常告诉我们:

一个函数只做一件事,如果几个函数都在做一件事,那么抽象出一个对象

是的,这些原则在业务逻辑开发中很有用,让代码更加容易维护,同时对性能几乎没有什么影响。但是,某些情况下它并不是毫无性能开销的。当执行太多的函数调用后,你会发现性能变得很慢。每当我减少一些函数调用(把函数中的代码直接拷贝到当前调用位置),性能都会有提升。

例如,在渲染引擎中经常会有矩阵计算,我们一般会抽象出一个matrix类:

matrix1.append(matrix2)

来实现矩阵相乘,但如果这段代码是在主渲染循环中,你可能就要考虑展开成如下的样子:

matrix1.a = matrix2.a * matrix1.a + matrix2.b * matrix1.c;
// ...

这样性能真的会有一些提升(大量执行的情况下),如果你的函数调用路径过长,建议进行一些流程简化。

除非极特殊的情况,要从架构上减少函数调用,不要粗暴地拆散函数

作用域链

js在查找变量的时候,会从当前作用域开始依次向上层查找。因此可以推断出,局部变量的访问永远是最快的,全局变量的访问永远是最慢的。

如下例:

var array = [];
function getName() {
    array[0] = 0;
    array[1] = 0;
    array[2] = 0;
    array[3] = 0;
    // ...
}

性能会低于:

var array = [];
function getName() {
    var array = array;
    array[0] = 0;
    array[1] = 0;
    array[2] = 0;
    array[3] = 0;
    // ...
}

如果在一个函数中多次调用全局变量或外层变量,记得先把它保存到局部变量。

this关键字

多次调用this关键字会让你的js执行很慢。因为当你访问一个对象属性,js执行时就要去查找原型链,直到查找到该属性为止。这个开销是很大的。

下面的例子:

for(var i = 0; i < 100; i++) {
    this.array[i] = i;
}

性能低于:

var array = this.array;
for(var i = 0; i < 100; i++) {
    array[i] = i;
}

如果多次访问对象属性(甚至循环调用),建议先把该属性保存到一个局部变量中,再使用。

数学运算

js的四则运算中,除法是最慢的,乘法其次。Math封装的数学函数中,sin与cos函数执行是最慢的。

下面的例子:

// a在大部分情况下为0
c = a * b;
f = a * e;

性能低于:

// a在大部分情况下为0
if(a == 0) {
    c = 0;
    f = 0;
} else {
    c = a * b;
    f = a * e;
}

尽量避免不必要的乘除运算,可能的情况下,缓存sin和cos运算结果。pixi.js中,显示对象的旋转要用到三角函数计算,引擎内部进行了标脏处理。egret中,对全局的三角函数计算方法进行了查表优化

在主循环方法中仔细查找,项目中可能存在很多类似的可优化点。

数组push与pop操作

大量调用数组push与pop方法,如果这些调用出现在循环中,那么很不幸,它会造成性能的下降。

如果在你的渲染循环中有这样的结构:

var matrix = Matrix.create();
// do something
Matrix.release(matrix);

并且对象池是这样实现的:

// 创建matrix对象
Matrix.create = function() {
    return Matrix.pool.pop() || new Matrix();
}
// 释放matrix对象
Matrix.release = function(matrix) {
    Matrix.pool.push(matrix);
}

那么你的主循环会被push与pop拖慢速度。

一般来说,引擎中会大量使用的临时对象。特定情况下,对于临时对象,除了使用对象池,还可以用一个全局的temp对象替代

例如,上例中,我们可以定义一个用于引擎内部临时使用的matrix对象 $tempMatrix:

Matrix.$tempMatrix = new Matrix();

使用的时候只需要:

var matrix = Matrix.$tempMatrix;
// do something
matrix.identify();

细心的同学可能会说,这样重复使用一个对象,如果代码逻辑上需要多个temp对象,上面的实现就不能解决需求了。的确,那我们就需要用其它的方式来解决。不过对于特定情况,上面这种优化是简单有效的。

总结

总结来看,这些所谓的性能优化点,大部分都是js语言在运行过程中的弱点,在其它语言中未必会重现。例如,上文提到的函数调用,在c++等语言中并不会对性能造成明显影响。

另外,如果大家做的不是类似于引擎这样的底层产品的话,这些东西了解一下也就得了。

再次强调,永远不要过度优化!!!