Chromeではメソッドをオブジェクトに直接入れてはいけない!?

Posted on

 短く言うと:アセンブラっぽく書く オブジェクト指向つかったら負け

 前の記事で書いた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__というように接頭辞を付けて区別してますがあまりにも不毛です


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください