JavaScriptのwithが遅いらしい話を検証してみた

ポエムばかり書いてるわけにもいかんので、新しい卒論の現実逃避をする。

CoffeeScriptの関数は明示的にreturnしてはいけない理由」を探す暇あったら他にやるべきことあるのでは? - mizchi's blog http://mizchi.hatenablog.com/entry/2013/12/16/184306

みたいな記事を書いてしまった以上、ダグラスクロックフォード卿の「JavaScriptのwithは遅いので使うべきではない」という神話を検証してみなければと思っていた。2014年においてはどうなんだろうか。

自分自身、たまにメタプロしたくなってwithを使いたい気分になることがある。John Resigとかはwith肯定派だと噂に聞いている。

実測

というわけで適当なスクリプトをでっちあげてベンチしてみる。環境は node v0.10.22

一番最初にでっち上げたコード

var f_with = function(n){
    var ret = 0;
    with(Math){
      ret = n * PI;
    };
    return ret;
};
 
var f = function(n){
    var ret = 0;
    ret = n * Math.PI;
    return ret;
};
 
console.time('f_with');
for(var i = 0; i < 1000000; i++) f_with(i);
console.timeEnd('f_with');
 
console.time('f');
for(var i = 0; i < 1000000; i++) f(i);
console.timeEnd('f');

withの名前の探索が最悪になるような組み方をしてみた。withの外のスコープのret, 引数のn, Mathに所属する PIをwithスコープの中で計算させてreturnする。同じロジックをwithを使わない版で書いて、ループぶん回して比較する。

結果

f_with: 357ms
f: 1ms

遅い (確信)

with使うとJITが効いてなさそうな感じがする。

さらに10倍ループしてみた

f_with: 3057ms
f: 5ms

このケースだと600倍ほど遅いってことがわかった。

わかりやすくしようとしたが…

最初、1msじゃ情報が少なすぎるので, べつの関数を定義してループ回数を増やしてみたら、よくわからない結果になった

var f_with = function(n){
        var ret = 0;
        with(Math){
                ret = n * PI;
        }
        return ret;
};

var f = function(n){
        var ret = 0;
        ret = n * Math.PI;
        return ret;
};
console.time('f_with');
for(var i = 0; i < 1000000; i++) f_with(i);
console.timeEnd('f_with');

console.time('f1000000');
for(var i = 0; i < 1000000; i++) f(i);
console.timeEnd('f1000000');

console.time('f100000000');
for(var i = 0; i < 100000000; i++) f(i);
console.timeEnd('f100000000');
f_with: 376ms
f1000000: 7ms
f100000000: 140ms

fに何の手も加えてないのにベンチが落ちた。実行回数を100倍にしても実測が100倍にならないは, なんらかのJITの最適化なんだろうが…

仮説を立てる

  • 初期化子 i のシンボル衝突で名前解決に悪影響が出た
  • f は一度しか呼ばれていないからインライン展開されていたが、複数回呼ぶことで最適化が崩れた

これらを修正する。 i, j, k でシンボルの衝突を避けて, f と全く同じ関数 g を定義することでインライン展開され高速化するのでは?と思ってやってみた。

var f_with = function(n){
        var ret = 0;
        with(Math){
                ret = n * PI;
        }
        return ret;
};

var f = function(n){
        var ret = 0;
        ret = n * Math.PI;
        return ret;
};

var g = function(n){
        var ret = 0;
        ret = n * Math.PI;
        return ret;
};

console.time('f_with');
for(var i = 0; i < 1000000; i++) f_with(i);
console.timeEnd('f_with');

console.time('f1000000');
for(var j = 0; j < 1000000; j++) f(j);
console.timeEnd('f1000000');

console.time('f100000000');
for(var k = 0; k < 100000000; k++) g(k);
console.timeEnd('f100000000');
~/s/coffeescript-to-typescript (master) $ node p.js 
f_with: 345ms
f1000000: 6ms
f100000000: 65ms

やっぱりよくわからなくなった。この挙動をたどるにはV8潜るしかなさそう。 わからんので誰か教えて(他力)

脱線したけどwithが遅いのはまあそうだとして、他の速度が十分で大域なwithじゃなけりゃいいんじゃねーかなと思ってしまうが、どうせろくな使われしないと思うし、さすがに600倍は遅すぎるので原則禁止ってのは妥当な範囲かなと思う。

まあライブラリ提供者が部分的にメタするぐらいにしておいたほうが良い。