編寫高性能對垃圾收集友好的代碼

RodTancred 8年前發布 | 10K 次閱讀 高性能 JavaScript開發 JavaScript

若你想讓你的游戲有60楨/秒的體驗,你必須要做的就是在16浩渺內完成所有事:子彈運動,創建實體,控制碰撞,軌跡,變換場景,控制輸入,播放音效。主流的游戲循環中,你需要做到盡可能高效。即便在30楨/秒的體驗中,你也只有32毫秒去完成這一切。特別是當你想要讓游戲更加豐滿時,速度與效率會顯得尤為重要。

垃圾回收究竟是什么?為何要關注垃圾回收?

如果你開發的游戲在同一時間內發生了許多事,例如每秒發射5次導彈的武器(一把有著極高射速的非凡武器)。你很快就會發現原型構建及其后的垃圾回收將嚴重拖累性能。

當你的項目變得越來越復雜,在構建新事物時,你將會感受到明顯的延遲,特別是當你設置復雜場景與音效時(即便你已經緩存了靜態資源)。創建多個對象同樣也會影響垃圾回收。

像其他解釋型語言一樣JavaScript把你從內存管理中解放出來。你可以隨意創建對象而不用去考慮追蹤的問題。瀏覽器(實際上是運行在瀏覽器環境中的js虛擬器)將會周期性運行并清除你不用的代碼。這部分系統就是垃圾回收(garbage collector)簡稱GC,你可以把它想象為終極女傭。

有賴于瀏覽器,你使用的大量的對象可以在垃圾回收機制下在10到2000毫秒內被清除。該機制花費的時間取決于,究竟有多少代碼需要檢查,有多少對象需要清除。如果你在寫一個有許多獨立運行的對象的游戲,例如作戰類游戲。最好的情況是可察覺的卡頓,而最壞的情況則是大量的卡頓毀掉了體驗。

有好的垃圾回收代碼

大多數情況有賴于垃圾回收機制,我們可以很輕易編寫代碼。唯一需要注意的就是不要應用那些你已經不需要使用的對象。例如下面的案例:

function test()
{
   var myString = 'a string';
}

test();

在函數 test 執行之后,變量 myString 將會被標記為閑置的等待釋放/刪除。因為函數創建了一個可聲明變量的作用域。在作用域中,瀏覽器會為開辟空間存放變量 myString 直到函數不再被使用,作用域中的一切都將被回收。在其后的某個時刻(由瀏覽器自行計算),GC將會執行,變量 myString 會“真地”被移除內存得到釋放。

當然如果還有引用,GC將不會回收變量,如:

var another = null;

function test()
{
    var str = 'A string I am';
    another = str;
}

test();

在上例中,全局作用域中的另一個變量在函數內引用了 str ,因此垃圾回收器將不會回收 str 。

再看看別的例子,當你不適用 var 關鍵字時,js會將其理解為一個全局變量:

// var b = null; // 在外部聲明

function test()
{
    var str = 'A string I am';
    b = str; // 沒有var 會被理解為全集變量
}

test();

如何真正地刪除變量?

那么問題來了,如何真實地了解到變量是否被刪除,即被垃圾回收器清理了?

var s = { data: 'test' };
delete s.data;

首先,JavaScript提供了 delete 關鍵字,所以可以用它來實現?不幸的是,不能。 delete 是用來清除對象屬性的。顯然這是一個間接清除對對象/變量清除引用的方式,但這并沒有直接刪除變量。當你想要把一個對象的屬性變成undefined而非null時,該方法還是挺有用的:

var s = { data: 'test' };
delete s.data;

s.data現在變為了 undefined (同時也被垃圾回收器標記為“去除”)

當你使用同樣的方式用 delete 去刪除一個變量時將會失敗:

var m = 'test';
delete m; // 默認返回 false (不允許操作)
m === 'test'; // true - oops, 依舊是那個值

delete 很好用,但對于一個變量(這里指基本類型)則會失效。因此引用并不會被清除,內存也不會被垃圾回收器回收。使用 delete 將返回false來表示這個變量不會被刪除。

刪除變量真確的方式是:把變量設為null,之后垃圾回收器就會去做他該做的事。

var m = 'test';
m = null;
m === 'test'; // false

該方法對屬性與對象同樣適用:

var s = { data: 'test' };
s.data = null;  // not required
s = null;   // 該操作同時也會清除s.data

其后數據將自動清除,因為沒有變量引用它了。

(譯注:這里清除的是變量中存放的數據)

避免創建對象

當然,降低垃圾回收最簡單的方式是不要去創建對象。最直接的方式就是使用 new 關鍵字。

var newObject = new MyObject();

但是還有一些更簡潔的方法:

var a = [];   // 創建一個數組
var t = { };  // 創建一個對象

需要記住的是 function 也會被當作對象處理:

function getCompare()
{
    return function(a, b) { return a < b; }
}

這段代碼在每次調用時都會生成一個新的(函數)對象。在JavaScript中由于函數是一個對象,所以垃圾回收器也能用同樣的方式處理。

另一種產生對象的方式是使用函數:

function getResult(}
{
    return { result: true, value: 'test' }; //調用時創建新的對象
}

最后,有一個藏得很深的方式:

var b = a.slice(1); // 創建一個新的完全復制的數組

Array.slice 方法在每次調用時會創建一個新的對象/數組。并不存在無痛地有效刪除數組中某個對象的方法。誠然現在瀏覽器在這方面做了很多優化,但頻繁進行這樣的操作對性能仍是一種挑戰。(在 Playcraft 引擎采用雙向鏈表作為替代,詳見gamecore.js)

可控的垃圾回收

即便你降低了你創建對象的數量,你依舊在消耗內存。你可以使用一個相當簡單的工具來了追蹤你消耗的內存即因而帶來的垃圾回收工作消耗的內存。

Chrome提供了一個觀測JavaScript堆(分配給JavaScript對象的內存)狀態的方式,你需要在命令行輸入一下代碼來保證其能夠運行:

chrome --enable-memory-info

如果你想要永久的使用這個工具,你可以這樣做:

do shell script 
  "\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\"     
      --enable-memory-info"

保存這段腳本并將其作為你新的Chrome launcher。

一旦你啟動了Chrome 的內存分析,你可以訪問如下兩個屬性:

window.performance.memory.totalJSHeapSize;  // 當前使用的堆內存
window.performance.memory.usedJSHeapSize; // 全部的堆內存

這兩個值表示當前有多少內存被分配給JavaScript,其中有多少被所有的變量/對象使用。如果你規律地輸出被使用的堆,你可以了解到你游戲的內存使用情況。

 

來自:https://segmentfault.com/a/1190000007887891

 

 本文由用戶 RodTancred 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!