资讯专栏INFORMATION COLUMN

如何编写避免垃圾开销的实时Javascript代码

Shisui / 2650人阅读

摘要:在语言中我们很难完全避免垃圾开销。它的垃圾收集模式在根本上是不符合像游戏这样的实时软件需求的。此外,在所有可能的情况下避免向量对象如中的和属性。

在 Javascript 语言中我们很难完全避免垃圾开销。它的垃圾收集模式在根本上是不符合像游戏这样的实时软件需求的。在这篇文章中我们主要介绍了一些关于 javascript 垃圾回收的方法。

编辑于 2012 年 3 月 27 日: 哇,这篇文章已经写了有很长一段时间了,十分感谢那些精彩的回复!其中有一些对于一些技术的指正,如使用 ‘delete’ 。我知道了使用它可能会导致其他的降速问题,因此,我们在引擎中极少使用它。一如既往的你还需要对所有的事进行权衡并且需要通过其他关注点来平衡垃圾回收机制,这也只是一个在我们引擎中发现的的实用、简单的技术列表,它并不是一个完整的参考列表。但是我希望它还是有用的!

一个用 Javascript 编写的 HTML5 游戏,要达到流畅体验的一个最大阻碍就是垃圾回收 ( GC ) 卡顿。 Javascript 并没有一个显式的内存管理,意味着你创造东西后却不能释放它们占用的内存。因此迟早浏览器便会替你决定去清理它们:这时代码执行就会被暂停,浏览器会找出哪一部分内存是现在仍在被使用的,并把其他所有东西占用的内存释放掉。这篇博文将会去探究避开GC开销的技术细节,这对方便进行使用任何插件或是使用 Construct 2 进行 Javascript SDK开发都应该能派上用场。

浏览器有很多技术性手段来减少 GC 卡顿,但是如果你的代码创造了许多垃圾,迟早浏览器也将会暂停并进行清理。随着对象逐步创建的过程中,之后浏览器又突然清理,这最后将导致内存使用情况图表呈现 z 字形。例如,下面是 Chrome 在玩太空爆破手时的内存使用情况。

当在玩一个 Javascript 游戏时会呈现 z 字形的内存占用情况。这可能是一个内存泄漏错误,但是实际上是 JavaScript 的正常操作。

此外,游戏以 60 fps 运行时只有 16 ms 的时间来渲染每一帧,但是 GC 会很轻易的产生最少 100 ms 以上明显的卡顿,在更糟的情况下,这会导致不断卡顿的游戏体验,因此对于像游戏引擎一样实时运行的 Javascript 代码,解决办法是努力尝试在典型帧的持续时间内你不要创建任何东西。这实际上是相当困难的,因为有许多看上去无害的 Javascript 语句实际上却创造了垃圾,它们都必须从每帧动画的代码路径里删除掉。在 Construct 2 中我们竭尽全力减少每一处引擎的垃圾开销,但是你可以从图表中看到上面仍然有许多小的对象被创建所以 Chrome 还会每隔数秒进行一次清除。要注意这里只是一个小的清理 - 这里并没有大量的内存被清理出来,因为一个更高更极端的z曲线会更引起关注,但是它可能已经足够好了,因为小型的垃圾集合执行会更快并且偶尔的小卡顿也一般不太引人注意 - 因此我们应该看到了,有时我们确实很难避免产生新的资源分配。

同样重要的包括第三方插件以及开发人员行为也需要遵守这些原则,否则,一个写的不好的插件可以产生许多垃圾并会让游戏十分卡顿,尽管主引擎 Construct 2 已经是一个非常低垃圾开销的引擎了。

简单的技巧

首先,最明显的是,关键词 new 指示了资源的分配,例如 new Foo() 在可能的情况下,它会在启动时尝试创建一个对象,并且尽可能长时间、简单的重新使用相同的对象。

不太明显的是,这里有三种快捷语法方式来相似的调用 new :

{} (创建一个新对象)
[] (创建一个新数组)
function () { ... } (创建一个新函数,也会被垃圾收集)

对于对象,用避免 {} 一样的方式来避免 new - 尝试去回收对象。请注意这包括像 { "foo": "bar" } 这样带属性的对象,也就是我们在函数中常用的一次性返回多个值。或许将每一次的返回值写入一个相同的(全局)对象来返回的写法是更好的 - 在文档中要仔细记录这一点,因为如果你保持引用这样的返回对象,可能在每次调用改变的时候发生错误。

实际上你可以回收一个存在的对象(如果它没有原型链)通过删除它的所有属性,将它还原为一个空的对象如 {} 一样。为此你可以使用 cr.wipe(obj) 函数,它的定义如下:

// remove all own properties on obj,
effectively reverting it to a new object
cr.wipe = function (obj)
{
    for (var p in obj)
    {
        if (obj.hasOwnProperty(p))
            delete obj[p];
    }
};

因此在某些情况下,你可以调用 cr.wipe(obj) 并为其再次添加属性来重用一个对象。比起重新简单分配 {} 现场清除一个对象可能需要更长的时间,但是在实时处理的代码中更重要的是避免产生垃圾,从而减少未来可能产生的卡顿情况。

分配 [] 到一个数组中被经常用来作为一个快捷方式去清除这个数组(例如 arr = [];),但请注意这将创建一个新的空数组并使旧的数组成为一个垃圾!更好的写法是 arr.length = 0; ,这种方式具有相同的效果但却继续使用了相同的数组对象。

函数则有一点棘手,函数通常在执行时创建并且不倾向于在运行时进行过多分配 - 但这意味着它们在动态创建时很容易被忽视。一个例子是返回函数的函数。主要的游戏循环使用了 setTimeout 或者 requestAnimationFrame 方法来调用一个成员函数类似如下:

setTimeout((function (self) { return function () {
self.tick(); }; })(this), 16);

这看起来像是一个合理的方式来每 16ms 调用一次 this.tick() 。然而,这也意味着每一次执行 tick 函数都会返回一个新函数!这可以通过永久存储函数的方法来避免,例如:

// at startup
this.tickFunc = (function (self) { return function () {
self.tick(); }; })(this);

// in the tick() function
setTimeout(this.tickFunc, 16); 

这将在每次执行 tick 函数时重复使用相同的函数来代替产生一个新的函数。这个方法可以应用到任意其他地方的返回函数中或是运行创建的函数中。

进阶技巧

随着我们的进展,进一步的避免产生垃圾变得更加困难,由于 Javascript 本身就是围绕着 GC 所设计的。许多 Javascript 中方便的库函数也总是创建了新的对象。这儿没有什么你可以做的但是当你返回文档查阅那些返回值时。例如,数组中的 slice() 方法会返回一个数组(基于保持不变的原始数组范围内),字符串的 substr 会返回一个新的字符串(基于保持不变的原始字符串字符的范围),等等。调用这些函数都会产生垃圾,而你能做的就是不要去调用它们,或是在极端情况下重写你的函数使它们不再产生垃圾。例如在 Construct 2 这种引擎,由于各种原因一个经常的操作是通过索引去删除数组里的一个元素。这个方法的快捷使用方式如下:

var sliced = arr.slice(index + 1);
arr.length = index;
arr.push.apply(arr, sliced);

然而 slice() 返回一个原始数组的后半部分来组成了一个新的数组,并且在被(arr.push.apply)复制后产生了垃圾。由于这是我们引擎中一个生产垃圾的热门处,它被改写为了一个迭代版本:

for (var i = index, len = arr.length - 1; i < len; i++)
    arr[i] = arr[i + 1];

arr.length = len;

显然重写大量的库函数是相当痛苦的,所以你需要仔细的权衡需求实现的方便性以及垃圾产生之间的平衡。如果它在每帧中被调用了很多次,你可能最好重写这个你需要的函数库。

这里可以很容易的使用 {} 语法来沿着递归函数传递数据。通过一个数组来表示一个堆栈,在这个堆栈中对递归的每一级进行 pushpop 是更好的。更好的是,实际上你并不需要在数组中 pop - 你应该将数组中最后一个对象像垃圾一样处理掉。来代替使用一个 ‘top index’ 变量进行简单减量。然后为了代替 pushing ,则增加 top index 并且如果有的话就重用数组中的下一个对象,否则执行真正的 push

此外,在所有可能的情况下避免向量对象(如 vector2 中的 x 和 y 属性)。虽然可能函数返回这些对象会让它们立刻改变或返回这两个值时会方便些,你可以在每一帧中轻松地结束数百个这样的创建对象,这将导致可怕的 GC 性能。这些函数必须分离出来在每个多带带的组件中工作,例如:使用 getX()getY() 来代替 getPosition() 来返回一个 vector2 对象。

有时候你无法摆脱一个库是一个产生垃圾的噩梦。 Box2Dweb 是一个典型的例子:它每一帧产生了数百个 b2Vec2 对象并且不断的在浏览器产生垃圾,并最终导致垃圾处理器产生显著的卡顿效果。在这种情况下最好的办法是创建一个缓存回收机制。我们一直在测试 Box2D(Box2Dweb-closure) 的修正版本,它似乎可以使 GC 暂停进行缓解(虽然没有完全解决)。查阅 b2Vec2.js 的 Get 和 Free 代码。这里有一个名字叫 ‘free cache’ 的数组,在之后的整个代码中如果不再使用 b2Vec2,它就会在 free cache 中被释放,当需要请求一个新的 b2Vec2,而它如果在 free cache 中还存在那么它就会被重用,否则才会分配一个新的。这并不完美,在一些测试后通常只有一半的 b2Vec2s 被创建并回收,但它确实帮助 GC 缓解了压力从而减少了频繁的卡顿。

结论

在 Javascript 中很难去完全避免垃圾。它的垃圾收集模式根本上是不符合像游戏这样的实时软件的需求的。从 Javascript 代码中需要进行大量的工作来消除垃圾,因为有很多直接的代码含有创建大量垃圾的副作用。然而,只要仔细小心一些,Javascript 也是可以在实时项目中不产生或是制造很少的垃圾开销,而对于需要保持高度响应性的游戏和应用程序这也是至关重要的。

原文链接 : How to write low garbage real-time Javascript
译文出自 : 掘金翻译计划

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79821.html

相关文章

  • [译文] JavaScript工作原理:V8引擎内部+5条优化代码窍门

    摘要:本文将会深入分析的引擎的内部实现。该引擎使用在谷歌浏览器内部。同其他现代引擎如或所做的一样,通过实现即时编译器在执行时将代码编译成机器代码。这可使正常执行期间只发生相当短的暂停。 原文 How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code 几周前我们开始了一个系列博文旨在深入...

    dreamans 评论0 收藏0
  • JavaScript如何工作:深入V8引擎&编写优化代码5个技巧

    摘要:第二篇文章将深入谷歌的引擎的内部。引擎可以实现为标准解释器,或者以某种形式将编译为字节码的即时编译器。这个引擎是在谷歌中使用的,但是,与其他引擎不同的是也用于流行的。一种更复杂的优化编译器,生成高度优化的代码。不是唯一能够做到的引擎。 本系列的 第一篇文章 主要介绍引擎、运行时和调用堆栈。第二篇文章将深入谷歌 V8 的JavaScript引擎的内部。 想阅读更多优质文章请猛戳GitHu...

    Turbo 评论0 收藏0
  • JavaScript如何工作:深入V8引擎&编写优化代码5个技巧

    摘要:第二篇文章将深入谷歌的引擎的内部。引擎可以实现为标准解释器,或者以某种形式将编译为字节码的即时编译器。这个引擎是在谷歌中使用的,但是,与其他引擎不同的是也用于流行的。一种更复杂的优化编译器,生成高度优化的代码。不是唯一能够做到的引擎。 本系列的 第一篇文章 主要介绍引擎、运行时和调用堆栈。第二篇文章将深入谷歌 V8 的JavaScript引擎的内部。 想阅读更多优质文章请猛戳GitHu...

    DevWiki 评论0 收藏0
  • Flink 源码解析 —— 深度解析 Flink 是如何管理好内存

    摘要:减少垃圾收集压力因为所有长生命周期的数据都是在的管理内存中以二进制表示的,所以所有数据对象都是短暂的,甚至是可变的,并且可以重用。当然,并不是唯一一个基于且对二进制数据进行操作的数据处理系统。 showImg(https://segmentfault.com/img/remote/1460000020044119?w=1280&h=853); 前言 如今,许多用于分析大型数据集的开源系...

    Edison 评论0 收藏0
  • JS中垃圾回收与内存泄漏

    摘要:介绍浏览器的具有自动垃圾回收机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。中的内存泄漏问题程序的内存溢出后,会使某一段函数体永远失效取决于当时的代码运行到哪一个函数,通常表现为程序突然卡死或程序出现异常。 showImg(https://segmentfault.com/img/remote/1460000018932880?w=4400&h=3080); 1. 介绍 浏...

    xiaolinbang 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<