読者です 読者をやめる 読者になる 読者になる

ES6速度測定(クラスその他編)

速度検証最終回。落ち葉拾い。

class構文

インスタンス生成とメソッド呼び出しを計測。

function FVector(x,y,z){ this.x=x; this.y=y; this.z=z; }
FVector.prototype.len=function(){
    return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z);
};
class CVector {
    constructor(x,y,z){ this.x=x; this.y=y; this.z=z; }
    len(){
        return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z);
    }
}
const cv=new CVector(1,2,3), fv=new FVector(1,2,3);
console.log(cv, fv);
const tests=[
    function() {    // new CVector
        return new CVector(3,4,5);
    },
    function() {    // new FVector
        return new FVector(3,4,5);
    },
    function() {    // CVector.distance()
        return cv.len();
    },
    function() {    // FVector.distance()
        return fv.len();
    }
];

測定結果(Chrome)は、

[1] x 25,869,050 ops/sec ±1.49% (61 runs sampled)
[2] x 52,926,253 ops/sec ±0.86% (63 runs sampled)
[3] x 53,630,570 ops/sec ±1.13% (57 runs sampled)
[4] x 52,670,978 ops/sec ±1.37% (62 runs sampled)

メソッド呼び出しは同等ながら、インスタンス生成はclassの方が遅い。性能を気にする場面でnewしまくる事もあまりないので実害はなさそうだが、

class CVector {
    // ...中略...
    sub(vec){
        return new CVector(this.x-vec.x, this.y-vec.y, this.z-vec.z);
    }
}

もし上みたいな実装をするならば、functionの方で書いた方がよさそう。

テンプレート文字列

const url='http://www.google.com';
const txt='Google';
const tests=[
    function() {   // template string
        return `<a href="${url}">${txt}</a>`;
    },
    function() {   // +で接続
        return '<a href="'+url+'">'+txt+'</a>';
    }
];

配列.join(‘’)は、さすがにこのケースでは勝負にならないので除外。

測定結果(Chrome)は、

[1] x 24,174,987 ops/sec ±0.88% (62 runs sampled)
[2] x 26,209,296 ops/sec ±0.68% (62 runs sampled)

可読性が高いのに性能はほぼ同等。素晴らしい。

String#startsWith(), String#endsWith()

const str='abcdefghijklmnopqrstuvwxyz'.repeat(100);
const tests=[
    function(){     // startsWith()
        return str.startsWith('abc');
    },
    function(){     // substr(0,l)
        const t='abc';
        return (str.substr(0,t.length)==t);
    },
    function(){     // endsWith()
        return str.endsWith('xyz');
    },
    function(){     // substr(-l)
        const t='xyz';
        return (str.substr(-t.length)==t);
    }
];

比較対象はsubstr()。イマドキのChromeではlastIndexOf()やindexOf()を使うよりsubstr()の方が速いので。

測定結果(Chrome)は、

[1] x 16,466,257 ops/sec ±1.08% (62 runs sampled)
[2] x 26,997,860 ops/sec ±0.85% (63 runs sampled)
[3] x 19,073,732 ops/sec ±1.00% (62 runs sampled)
[4] x 23,324,930 ops/sec ±0.93% (60 runs sampled)

substr()が速いのか、それとも他の関数が遅いのか。

Math.hypot()

const pt={ x:12.3,y:45.6,z:78.9 };
const tests=[
    function(){ return Math.hypot(pt.x,pt.y) },
    function(){ return Math.hypot(pt.x,pt.y,pt.z) },
    function(){ return Math.sqrt(pt.x*pt.x+pt.y*pt.y) },
    function(){ return Math.sqrt(pt.x*pt.x+pt.y*pt.y+pt.z*pt.z) },
];

hypotはhypotenuse=斜辺。つまり意味的には2次元。JavaのMath.hypot()も引数2つ。 しかしjavascriptのMath.hypot()は可変長引数なのだ。

測定結果(Chrome)は、

[1] x 5,796,367 ops/sec ±0.92% (62 runs sampled)
[2] x 5,583,658 ops/sec ±0.90% (63 runs sampled)
[3] x 57,648,690 ops/sec ±1.93% (61 runs sampled)
[4] x 57,209,776 ops/sec ±1.27% (61 runs sampled)

圧倒的な遅さ。可変長引数という点も少しは影響してるだろうが、遅い理由の大部分はおそらく、JavaのMath.hypot()と同じ事情。sqrt()内の計算結果がdoubleの表現範囲からオーバーフローしないよう、面倒な事をしてくれているらしい。実際、Math.hypot(1e300,2e300)としても正しい結果が返ってくる。上の[3],[4]で同じ事をやればInfinityになる。日頃そんな計算はしそうにないけど、最小公倍数的な実装をしなければいけないのが共通関数のつらいところ。

その他

以下も一応計測はしたけど、あまりに当たり前すぎたので結論だけを書いておく。

  • アロー関数は、let self=this的な事をした場合と同等の性能。つまり、thisを拘束する必要がないなら(...)=>{...}の形よりfunction(...){...}の形にした方が僅かに速い。
  • デフォルトパラメータは、関数本体で引数 || デフォルト値的な事をした場合と同等の性能。当然、引数の省略を認めない方が僅かに速い。

ES6速度測定(連想配列編)

連想配列について。前回のArrayも連想配列だろ、なんて細かい事はここでは言わないのが文脈ってもの。

ObjectとMap

連想配列内全値の合計処理で計測。

const obj={},map=new Map();
{
    let i, key;
    for(i=256;i--;){
        key=i.toString(16);
        obj[key]=i;
        map.set(key, i);
    }
    console.log(obj, map);
}
const tests=[
    function() {    // for-in loop
        let k, sum=0, o=obj;
        for(k in o) sum+=o[k];
        return sum;
    },
    function(){     // Object keys
        let sum=0,o=obj;
        for(let k of Object.keys(o)) sum+=o[k];
        return sum;
    },
    function() {    // Object.values
        let sum=0;
        for(let v of Object.values(obj)) sum+=v;
        return sum;
    },
    function() {    // Object.entries
        let sum=0;
        for(let kv of Object.entries(obj)) sum+=kv[1];
        return sum;
    },
    function() {    // Map keys
        let sum=0,m=map;
        for(let k of m.keys()) sum+=m.get(k);
        return sum;
    },
    function() {    // Map values
        let sum=0;
        for(let v of map.values()) sum+=v;
        return sum;
    },
    function() {    // Map entries
        let sum=0;
        for(let kv of map.entries()) sum+=kv[1];
        return sum;
    },
    function() {    // Map iterator
        let sum=0;
        for(let kv of map) sum+=kv[1];
        return sum;
    }
];

ソース中、for(let kv of ...)のところ、for(let [k,v] of ...)とすると、僅かではあるが遅くなるようなので、kvの方で書いた。

結果はもうChromeだけで。

[1] x 76,441 ops/sec ±0.91% (61 runs sampled)
[2] x 90,609 ops/sec ±1.06% (60 runs sampled)
[3] x 20,534 ops/sec ±0.84% (62 runs sampled)
[4] x 15,424 ops/sec ±0.82% (62 runs sampled)
[5] x 45,595 ops/sec ±1.28% (61 runs sampled)
[6] x 85,293 ops/sec ±0.86% (63 runs sampled)
[7] x 85,273 ops/sec ±0.81% (64 runs sampled)
[8] x 84,741 ops/sec ±0.81% (63 runs sampled)

予想通りというか、Object.keys()でfor-ofするのが速い。Object.values()やentries()はes6ですらない未来の機能のフライング実装なのでまぁこんなものか。Mapはイテレーションには強いもののObject.keys()には及ばず、結局Map.get()が足を引っ張るため使えないという結論に。for-inはプロトタイプまで取りに行くので、こういう場面での性能はイマイチ。

Object.assign()

連想配列のシャローコピーで比較。

const tests=[
    function(){     // Object keys
        let ret={},o=obj;
        for(let k of Object.keys(o)) ret[k]=o[k];
        return ret;
    },
    function() {    // Object.assign
        return Object.assign({},obj);
    }
];

結果は

[1] x 49,171 ops/sec ±0.95% (63 runs sampled)
[2] x 12,115 ops/sec ±0.78% (62 runs sampled)

Object.assign()は可変長引数だから期待はしてなかったけど、やはり遅い。

ES6速度測定(配列編)

今こんな事やっても気が早いというか、v8のes6まわりの進化に伴ってどうせすぐに結果が変わる。それはわかってるんだけど、ともかく今es6使うから今速度計測してみるのだ。

for-of ループ

配列の総和を求める関数で計測。ソースはこんな感じ

const arr=(function(){
    let i, ret=[];
    for(i=1000;i--;) ret[i]=i;
    return ret;
})();
console.log('arr',arr);

const tests=[
    function() {  // いつものループ
        let i, sum=0, a=arr, n=a.length;
        for(i=0; i<n; i++) sum+=a[i];
        return sum;
    },
    function() {  // for(var i=0;...) を機械的にletに 
        let sum=0, a=arr, n=a.length;
        for(let i=0; i<n; i++) sum+=a[i];
        return sum;
    },
    function() {  // よくある逆順回し
        let i, sum=0, a=arr;
        for(i=a.length;  i-- >0;) sum+=a[i];
        return sum;
    },
    function() {  // for...of登場
        let v, sum=0, a=arr;
        for(v of a) sum+=v;
        return sum;
    },
    function() {  // for...ofでもletしてみる
        let sum=0, a=arr;
        for(let v of a) sum+=v;
        return sum;
    }
]

これを benchmark.js を使って

for(let i=0,n=tests.length; i<n; i++){
    console.log(i+1, tests[i]());
    suite.add('['+(i+1)+']', tests[i]);
}
suite.run({ 'async': true });

で実行。結果は、Chrome 57.0.2987.133(v8 5.8.283.32)では、

[1] x 100,884 ops/sec ±1.39% (62 runs sampled)
[2] x 58,742 ops/sec ±0.94% (62 runs sampled)
[3] x 106,726 ops/sec ±0.84% (63 runs sampled)
[4] x 536,084 ops/sec ±0.85% (63 runs sampled)
[5] x 533,581 ops/sec ±0.99% (63 runs sampled)

Electron 1.6.5(v8 5.6.326.50)では、

[1] x 104,903 ops/sec ±0.50% (89 runs sampled)
[2] x 58,469 ops/sec ±0.61% (87 runs sampled)
[3] x 106,472 ops/sec ±0.78% (85 runs sampled)
[4] x 112,681 ops/sec ±0.61% (88 runs sampled)
[5] x 112,252 ops/sec ±1.25% (84 runs sampled)

となった。 ちなみに手元のnode.jsはv7.9.0でv8のversionは5.5.372.43ともっと古いので計測してない。

for-ofはchromeでは圧倒的。electronでは従来並だけど、v8のバージョンがchromeのものより少し古いせいだとすれば、バージョンアップに伴って速くなるかも。実はこれがトランスパイラなしでes6を使ってみる気になった理由だ。

注意したいのは、2番目のようなletの使い方。ループ内が別スコープになるのでsumなどへのアクセスが遅くなる・・・にしては速度低下が極端な気もするが、ともかく、既存ソースのvarを機械的にletに書き換えたらこの形になる心当たりはありまくる。気をつけよう。 ちなみに5番目の、for-ofでletしたケースでは遅くはなっていない。for-ofでletできる変数って、(実装は知らないけど意味的には)変数というよりも配列各要素のビューだから、上手いことやれるんだろう。

おまけ

遅かった2番目、単一スコープに無理やり押し込んで

function() {
    let o={sum:0};
    for(let i=0, a=arr, n=a.length, s=o; i<n; i++) s.sum+=a[i];
    return s.sum;
}

という形にしたら

[6] x 304,702 ops/sec ±0.76% (63 runs sampled)

なにそれ速い。どゆこと。

spread演算子

一緒くたにしてしまったけど、最初3ケースは配列のシャローコピー、後ろ3ケースは最大値特定処理の計測。

const tests=[
    function(){  // concat()で複製
        return arr.concat();
    },
    function(){  // spread演算子で複製
        return [...arr];
    },
    function(){  // push()にspread
        let ret=[];
        ret.push(...arr);
        return ret;
    },
    function(){  // ループで最大値を求める
        let i, max=arr[0], a=arr, n=a.length;
        for(i=1; i<n; i++) if(a[i]>max) max=a[i];
        return max;
    },
    function(){  // Math.max()にspread
        return Math.max(...arr);
    },
    function(){  // apply()
        return Math.max.apply(Math, arr);
    }
];

結果はChrome

[1] x 907,373 ops/sec ±1.09% (63 runs sampled)
[2] x 21,710 ops/sec ±0.77% (62 runs sampled)
[3] x 318,282 ops/sec ±0.54% (63 runs sampled)
[4] x 882,226 ops/sec ±0.87% (61 runs sampled)
[5] x 338,162 ops/sec ±0.82% (63 runs sampled)
[6] x 313,212 ops/sec ±1.02% (61 runs sampled)

electronで

[1] x 903,084 ops/sec ±0.74% (85 runs sampled)
[2] x 19,549 ops/sec ±0.51% (87 runs sampled)
[3] x 29,599 ops/sec ±0.47% (87 runs sampled)
[4] x 911,266 ops/sec ±0.44% (86 runs sampled)
[5] x 31,114 ops/sec ±0.41% (89 runs sampled)
[6] x 31,028 ops/sec ±0.45% (87 runs sampled)

結局、配列コピーはconcat()が最速で、最大値特定はループで書くのが最速。それらと比べれば、spread演算子は性能面ではイマイチ。Chromeでは、spread演算自体はそこそこのレベルまで高速化されているようだが、[2]の使い方では酷く遅い。electronでは、chromeと性能が1桁違う。地味にapply()も1桁違うところをみると、spread演算子の高速化にあわせてapply()も速くなったという事だろうか。

とにかく一番短い書き方が最速になってほしいものだけど、そう上手くはいかないみたい。

生ECMAScript6開発環境

ECMAScript6は、現在はまだトランスパイラで書く時代なんだろうけど、electron用ならes6をそのまま実行したい気もしたので環境構築してみたメモ。

 

エディタ

求めるもの

(1) ES6構文の色付け

これだけ。

ところであまり関係ないけど、エディタで括弧なり引用符入れると対応する括弧なり引用符が自動的に入力される機能、あれを邪魔だと思うのは私だけ?どのエディタでもデフォルトONでツライ。

現実

(1) 各種トランスパイラが普及してるだけあって、atomでもvs codeでも問題なし。

これまた関係ないけど、vs codeの.vssettingsフォルダが邪魔というだけの理由で私はatom派。vimプラグインはvs code版の方が優秀っぽいんだけどね。

 

minifier

javascriptのminifierって、日本語ではどう呼ぶのが一般的なのやら。「軽量化ツール」?

求めるもの

(1) 空白文字削除

 これだけは必須。他は「あればいいな」程度。
 他人にあまり簡単にソースを読まれたくないリリース物ってのはあるわけで。

(2) 変数名を短いものにする

 どうせ(1)をやるなら、なるべく小さくなるべく難読化を。

(3) 定数同士の演算を1つの定数にまとめる

 例えば「ラジアンで90度」という意図ならば、可読性の観点から1.57ではなく3.14/2と書きたい。しかし速度にこだわる場面では逐一計算されないか不安。実行時最適化されそうな気もするけど、minify時に1.57に書き換わるのを確認できた方が安心。

(4) 定数と関数のインライン展開

 ちょっとした計算を関数化した方が読みやすい。でも速度にこだわるなら関数化で遅くなるというジレンマに対する処方。

現実

これこそが今日の本題。なにせ有名どころが全然es6に対応してない。(4)なんかはもうClosure Compilerそのものだが、現状Closure Compilerはes6を出力しない。

最終的にはuglifyJS2のharmonyブランチを使うことにした。versionは2.8.22。そもそもexperimentalという位置づけだし、しっかりバグにも遭遇したけど、それでも(1)だけなら使えそう。(2),(3)はこの完成度では敬遠しておく。(4)はそもそも機能自体ない。

ちなみに遭遇したバグというのは、rest parametersを使った

function f(v,....arr){}

みたいな関数の呼び出し側が、

f(0,1,2,3)

から

f(0,1)

に書き換わってるというもの。無茶しやがって。まあ配列渡しで書けばOK。

 

ビルド

es6のimport/exportは一部のトランスパイラくらいでしか使えないらしいけど、

uglifyjs -e -m -c -o main.js *.js

みたいに1つのjsにすればまぁいいよね。