JavaScript(V8)で避けるべき(だった?)クロージャの使い方

Grokking V8 closures for fun (and profit?) に、ほんの少しだけ触れられている話なんですが。

ごく最近まで V8 には、オブジェクトリテラルの中で関数リテラルを使った場合に非常に遅くなる(というかGCが多発する)問題があった。

たとえば、

function doit() {
    for (var i = 0; i < 1000; ++i) {
	for (var j = 0; j < 1000; ++j) {
	    var o = {
		f: function () {
			return i + j;
		}
	    };
	}
    }
}
doit();

というコードを node-0.6.19 で実行すると、以下のように mark-sweep GC が大量に発生して処理に時間がかかっていることが分かる。

$ time /usr/local/node-0.6.19/bin/node --trace-gc ~/tmp/closure-leak.js 
Scavenge 2.3 -> 1.9 MB, 0 ms.
Scavenge 2.7 -> 2.0 MB, 0 ms.
Mark-sweep 3.1 -> 1.7 MB, 1 ms.
Scavenge 4.0 -> 2.2 MB, 0 ms.
(中略)
Mark-sweep 11.7 -> 1.6 MB, 3 ms.
Mark-sweep 19.8 -> 1.6 MB, 4 ms.
Mark-sweep 19.8 -> 1.6 MB, 5 ms.
Mark-sweep 19.8 -> 1.6 MB, 4 ms.
Mark-sweep 19.8 -> 1.6 MB, 4 ms.
Mark-sweep 19.8 -> 1.6 MB, 4 ms.


real	0m0.892s
user	0m0.871s
sys	0m0.026s

ちなみに最新の V8 では、このコードを実行するのに 30ms 程度しかかかりません。30倍の高速化www

最新版で修正されているとは言え、ブラウザがアップデートされない Android では、JavaScript コード側での対応が引き続き必要な案件のひとつですね。以下のように書くことで問題を回避しましょう。

function doit() {
    for (var i = 0; i < 1000; ++i) {
	for (var j = 0; j < 1000; ++j) {
	    var f = function () {
		return i + j;
	    };
	    var o = {
		f: f
	    };
	}
    }
}
doit();

20:54追記: free variable の有無に関係ない話なので、例をかえました