短く言うと:アセンブラっぽく書く オブジェクト指向つかったら負け
もくじ
■
前の記事で書いたJavaScriptのファミコンエミュレータ「CycloaJS」を実装する際に使った最適化の手法について紹介します。
■関数呼び出し、避けましょう
まずはこんな関数から〜。
"use strict"; var TIMES = 100000000; function Klass(){ this.val = 0; this.add = function(val){ this.val += val; }; } function doBench() { var memory = new Uint8Array(1024*1024); log("calling func: "+cycloa.probe.measure(function(){ var obj = new Klass(); var i = TIMES; while(--i){ obj.add(i&0xfff); } })); log("not calling func: "+cycloa.probe.measure(function(){ var obj = new Klass(); var i = TIMES; while(--i){ obj.val += (i&0xfff); } })); }
cycloa.probe.measureっていうのは、実行時間を測ってミリ秒単位で返してくれるだけのユーティリティ関数です。最初のベンチマークでは変数に足し算する処理をクラスメソッドに移譲していますが、次のベンチマークでは移譲せずに直接書いてます。こんな例だと馬鹿馬鹿しいですが、規模が大きくなると前者みたいな方式の方が見通しがよくなったりするんですよね…。
これを、Chrome21とFirefox14.0.1を用いてログを取ってみました。
■Firefox14.0.01
calling func: 657 not calling func: 289 calling func: 772 not calling func: 284 calling func: 768 not calling func: 293
関数呼び出しを加えただけでかなり遅くなります。C/C++やJavaのような感覚でgetter/setterなどを作ると一気に遅くなってしまいます。
今回エミュレータでは、CPUのエミュレーション部分がメモリの読み書きを行う際のメソッドを、すべてコードジェネレータを用いてインライン展開しています。
同様に、その他の通常ならば別のメソッドに分けるような処理も、すべて一つの関数内で行うようにしました。erbを用いて埋め込むなどしてできる限り可読性を下げないようにはしているものの、正直見難くなっているのが実情です…。
■Chrome21
calling func: 592 not calling func: 592 calling func: 1327 not calling func: 590 calling func: 1035 not calling func: 587 calling func: 1037 not calling func: 585
Chromeだと、最初の一回目だと差が出ない(インライン展開でも行なっているのでしょうか?)のですが、二回目以降関数呼び出しバージョンが極端に遅くなります。なぜだろと思ってプロファイラを有効にすると、なぜか差がでなくなってしまいます…。なぜなのかはよく分かりません。試しに、
var obj = new Klass(); function doBench() { var memory = new Uint8Array(1024*1024); log("calling func: "+cycloa.probe.measure(function(){ var _obj = obj; var i = TIMES; while(--i){ _obj.add(i&0xfff); } })); log("not calling func: "+cycloa.probe.measure(function(){ var _obj = obj; var i = TIMES; while(--i){ _obj.val += (i&0xfff); } })); }
として、オブジェクトの生成を一度だけにしたとしても…
calling func: 575 not calling func: 565 calling func: 890 not calling func: 577 calling func: 881 not calling func: 572
として、二度目以降やっぱり関数呼び出しバージョンが遅くなってしまいます。なぜでしょう…。
■ローカル変数、使いましょう
お次はこのコードです。
- グローバル変数
- ローカル変数(var variable = 0;)
- thisオブジェクトのメンバ変数(this.variable)
- ローカルオブジェクトのメンバ変数(var obj = 略;obj.variable)
- thisオブジェクトのメンバオブジェクトのメンバ変数(this.obj.variable)
時間[ms] | Chrome21 | Firefox14 |
---|---|---|
グローバル変数 | 302 | 622 |
ローカル変数 | 132 | 255 |
this.variable | 586 | 280 |
var obj;obj.variable | 580 | 280 |
this.obj.variable | 655 | 292 |
ChromeとFirefoxで随分特性が違いますが、できる限りローカル変数を使うのが一番なのは間違いありません。
定数などはよくグローバル変数としてこのように、擬似名前空間を作った上で保持しておくのが割と綺麗な設計だと思いますが…
var namespace = {}; //ソフトウェア用の擬似名前空間 namespace.INT_MAX=0xffffffff; //32bitIntegerの最大値 namespace.Klass = function(){ this.method = function(){ var variable = namespace.INT_MAX; //定数を参照 for(***){ variable = (variable+1) & namespace.INT_MAX; //32ビットカウンターとして使う } }; };
以上のように複数回定数を使う場合は、
var namespace = {}; //ソフトウェア用の擬似名前空間 namespace.INT_MAX=0xffffffff; //32bitIntegerの最大値 namespace.Klass = function(){ this.method = function(){ var tmpINT_MAX = namespace.INT_MAX; //一旦ローカル変数にコピー var variable = tmpINT_MAX; //上記のローカル変数の方を参照している for(***){ variable = (variable+1) & tmpINT_MAX; //こちらもローカル変数を参照 } }; };
このように一旦ローカルに保持しています。…これだけでかなり見づらくなるのでおすすめしません…。
さらに書き換えられる事の多い変数に関しては、
this.method = function(){ //一旦ローカルにコピーする var variable = this.variable; //いっぱい書き換えられる variable += this.calc1(); variable += this.calc2(); //thisのメンバ変数に戻す this.variable = variable; }
のようにして、一旦ローカル変数にコピーして最後に再度戻すといった事もしています。書き戻す前にthis.variableにアクセスすると分かりづらいバグになるので非常に注意です。なんてまあ不毛なことを…
さらに、機能ごとにオブジェクトを分割、たとえば今回はエミュレータなのでCPUとサウンドとビデオにクラスを分けて実装してしまうと…
function Video(){ this.method = new run() { //描画 }; this.parameter = 0; //何か } function CPU(){ this.method = new run() { //CPUの命令実行 }; this.parameter = 0; //何か } function Emulator(){ this.videoModule = new Video(); this.cpuModule = new CPU(); this.run = function(){ this.videoModule.run(); this.cpuModule.run(); }; }
このように、this.subModule.run()のように呼びださなくてはいけなくなってしまい、1秒間に数千回呼ぶとコストが馬鹿にならないため…
function Emulator(){ this.videoModule = new Video(); this.cpuModule = new CPU(); this.run = function(){ //描画処理 //... //CPUの命令実行 //... }; //変数名の衝突を回避 this.__video__parameter = 0; this.__cpu__parameter = 0; }
というようにしてしまっています。こうすると1番目の関数呼び出しのコストも避けられます…が、まずモジュールごとの名前空間が無くなるため、そこに気を付けなければなりません。今回の例ではVideoとCPUのparameterという変数名が衝突しているため、__cpu__と__video__というように接頭辞を付けて区別してますがあまりにも不毛です。