[Node.js][Deno] クラス定義各種 ベンチマーク比較

JavaScriptのクラス定義手法はいっぱいあって、これを解説している記事もいっぱいありますが、パフォーマンスに言及しているところがなかったので、自前で実験してみた。

# 実験環境

- Node.js 14.15.1
- Deno 1.5.4

なお、webクライアントでの実行コストはあまり気にしてないので、各種ブラウザでの比較はパス。

# 実験設定

- 継承のあるクラスで
  - インスタンス生成コストだけでなく、親メンバへのアクセスコストも重要
- とにかくランタイムで高速なものを追求
  - プロトタイプ定義コストは、あまり気にしてない
- 標準的なクラス定義方法との互換性も、あまり気にしてない
  - 各メンバの読み書きができれば充分
  - ただしもちろん、書き込みによってプロトタイプを破壊しないことは重要

そんなわけで、各手法それぞれ

- TestClassA 定義
  - プロパティ a とメソッド getA() を含める
- TestClassB 定義
  - TestClassA を継承
  - プロパティ b~i を含める
- TestClassC 定義
  - TestClassB を継承
  - プロパティ j~z を含める
    (このぐらい余計に定義入れとけばプロトタイプへのアクセスコストに差が出てくること期待)
- TestClassC で最初のインスタンス生成
  - ついでに正常にアクセスできるかテスト
- 実行時間計測
  - 最初のインスタンスでget (プロパティ a 読み出し)
  - 最初のインスタンスでcall (メソッド getA() 呼び出し)
  - 新規生成インスタンスでget
  - 新規生成インスタンスでcall
- 結果表示
  - get call それぞれ 
  - 新規生成インスタンス版結果 - 最初のインスタンス流用版結果 → インスタンス生成コスト
  - 10回実行して平均をとる

な手順で。

# 実験: 標準的なprototype定義方式

まずは普通に。

``` javascript:bench_class_standard.js
// TestClassA 定義 
function TestClassA(){
	this.a=1;
};
TestClassA.prototype.getA=function(){return this.a;};

// TestClassB 定義 
function TestClassB(){
	TestClassA.call(this);
	this.b=2;
	this.c=3;
	this.d=4;
	this.e=5;
	this.f=6;
	this.g=7;
	this.h=8;
	this.i=9;
}
Object.setPrototypeOf(TestClassB.prototype,TestClassA.prototype);

// TestClassC 定義 
function TestClassC(){
	TestClassB.call(this);
	this.j=10;
	this.k=11;
	this.l=12;
	this.m=13;
	this.n=14;
	this.o=15;
	this.p=16;
	this.q=17;
	this.r=18;
	this.s=19;
	this.t=20;
	this.u=21;
	this.v=22;
	this.w=23;
	this.x=24;
	this.y=25;
	this.z=26;
}
Object.setPrototypeOf(TestClassC.prototype,TestClassB.prototype);

// 既存インスタンス準備 
var t=new TestClassC();
//console.log(t);
//console.log(t.getA());

// 実行内容 
var funx={
	dmy:()=>0, // 最初の実行項目は不利な結果が出るのでダミー 
	get:()=>t.a,
	call:()=>t.getA(),
	new_get:()=>new TestClassC().a,
	new_call:()=>new TestClassC().getA(),
};
// 計測結果を書き込むところ 
var rec={};

// 繰り返し実行時間計測 
var loop=Array(10000000);
for(var k in funx){
	var f=funx[k];
	var a=0;
	var bgn=new Date;
	for(var i of loop)a+=f();
	var end=new Date;
	rec[k]=end-bgn;
}

// インスタンス生成時間を抽出表示 
console.log(rec.new_get-rec.get);
console.log(rec.new_call-rec.call);
```

|  | Node.js | Deno |
|:--|--:|--:|
| get | 3.7 | 427.8 |
| call | 7.7 | 434.7 |

後発品なのになんでこんな遅いんだよDeno
…というわけで詳細に調べたところ、どうやらコンストラクタで親クラスのcall呼んでるところでやたらと時間かかってる模様。
そんなわけでDeno移行は時期尚早かな…って結論出す前に、まず次いってみましょうか。

# 実験: いまどきのclass方式

ECMAScript 2015以降の追加仕様ですね。
めんどいprototype定義ともおさらば、やったー
…って喜ぶ前に、パフォーマンスをみてみましょう。

``` javascript:bench_class_modern.js
// TestClassA 定義 
class TestClassA{
	constructor(){
		this.a=1;
	}
	getA(){return this.a;}
}

// TestClassB 定義 
class TestClassB extends TestClassA{
	constructor(){
		super();
		this.b=2;
		this.c=3;
		this.d=4;
		this.e=5;
		this.f=6;
		this.g=7;
		this.h=8;
		this.i=9;
	}
}

// TestClassC 定義 
class TestClassC extends TestClassB{
	constructor(){
		super();
		this.j=10;
		this.k=11;
		this.l=12;
		this.m=13;
		this.n=14;
		this.o=15;
		this.p=16;
		this.q=17;
		this.r=18;
		this.s=19;
		this.t=20;
		this.u=21;
		this.v=22;
		this.w=23;
		this.x=24;
		this.y=25;
		this.z=26;
	}
}

// 既存インスタンス準備 
var t=new TestClassC();
//console.log(t);
//console.log(t.getA());

// 実行内容 
var funx={
	dmy:()=>0, // 最初の実行項目は不利な結果が出るのでダミー 
	get:()=>t.a,
	call:()=>t.getA(),
	new_get:()=>new TestClassC().a,
	new_call:()=>new TestClassC().getA(),
};
// 計測結果を書き込むところ 
var rec={};

// 繰り返し実行時間計測 
var loop=Array(10000000);
for(var k in funx){
	var f=funx[k];
	var a=0;
	var bgn=new Date;
	for(var i of loop)a+=f();
	var end=new Date;
	rec[k]=end-bgn;
}

// インスタンス生成時間を抽出表示 
console.log(rec.new_get-rec.get);
console.log(rec.new_call-rec.call);
```

|  | Node.js | Deno |
|:--|--:|--:|
| get | 145.4 | 91.3 |
| call | 139.8 | 89.3 |

…遅っ。
だめだこりゃ、次いってみよー

# 実験: コンストラクタをinit()で代用方式

先ほど、親クラスのcallが遅いって書きました。
で、コンストラクタ使わずに自前で初期化メソッド書いちゃえばいんじゃね作戦。

``` javascript:bench_class_noctor.js
// TestClassA 定義 
function TestClassA(){}
TestClassA.prototype.init=function(){
	this.a=1;
};
TestClassA.prototype.getA=function(){return this.a;};

// TestClassB 定義 
function TestClassB(){}
Object.setPrototypeOf(TestClassB.prototype,TestClassA.prototype);
TestClassB.prototype.init=function(){
	Object.getPrototypeOf(this).init();
	this.b=2;
	this.c=3;
	this.d=4;
	this.e=5;
	this.f=6;
	this.g=7;
	this.h=8;
	this.i=9;
};

// TestClassC 定義 
function TestClassC(){}
Object.setPrototypeOf(TestClassC.prototype,TestClassB.prototype);
TestClassC.prototype.init=function(){
	Object.getPrototypeOf(this).init();
	this.j=10;
	this.k=11;
	this.l=12;
	this.m=13;
	this.n=14;
	this.o=15;
	this.p=16;
	this.q=17;
	this.r=18;
	this.s=19;
	this.t=20;
	this.u=21;
	this.v=22;
	this.w=23;
	this.x=24;
	this.y=25;
	this.z=26;
};

// 既存インスタンス準備 
var t=new TestClassC();
t.init();
//console.log(t);
//console.log(t.getA());

// 実行内容 
var funx={
	dmy:()=>0, // 最初の実行項目は不利な結果が出るのでダミー 
	get:()=>t.a,
	call:()=>t.getA(),
	new_get:()=>{var u=new TestClassC(); u.init(); u.a},
	new_call:()=>{var u=new TestClassC(); u.init(); u.getA()},
};
// 計測結果を書き込むところ 
var rec={};

// 繰り返し実行時間計測 
var loop=Array(10000000);
for(var k in funx){
	var f=funx[k];
	var a=0;
	var bgn=new Date;
	for(var i of loop)a+=f();
	var end=new Date;
	rec[k]=end-bgn;
}

// インスタンス生成時間を抽出表示 
console.log(rec.new_get-rec.get);
console.log(rec.new_call-rec.call);
```

|  | Node.js | Deno |
|:--|--:|--:|
| get | 1857.6 | 1949.5 |
| call | 1848.6 | 1949.3 |

OMG.
getPrototypeOf() さらに重かった。
しかもこれ、 console.log(t) でTestClassCのぶんしか出ないですよ。
console.log(t.getA()) はちゃんと1って出てるので、継承は正常っぽい。

# 実験: 親クラスのメソッド転記方式

では、 getPrototypeOf() にも頼らず、親クラスのメソッドを自前転記作戦。

``` javascript:bench_class_selflink.js
// TestClassA 定義 
function TestClassA(){}
TestClassA.prototype.init=function(){
	this.a=1;
};
TestClassA.prototype.getA=function(){return this.a;};

// TestClassB 定義 
function TestClassB(){}
Object.setPrototypeOf(TestClassB.prototype,TestClassA.prototype);
TestClassB.prototype.init_TestClassA=TestClassA.prototype.init;
TestClassB.prototype.init=function(){
	this.init_TestClassA();
	this.b=2;
	this.c=3;
	this.d=4;
	this.e=5;
	this.f=6;
	this.g=7;
	this.h=8;
	this.i=9;
};

// TestClassC 定義 
function TestClassC(){}
Object.setPrototypeOf(TestClassC.prototype,TestClassB.prototype);
TestClassC.prototype.init_TestClassB=TestClassB.prototype.init;
TestClassC.prototype.init=function(){
	this.init_TestClassB();
	this.j=10;
	this.k=11;
	this.l=12;
	this.m=13;
	this.n=14;
	this.o=15;
	this.p=16;
	this.q=17;
	this.r=18;
	this.s=19;
	this.t=20;
	this.u=21;
	this.v=22;
	this.w=23;
	this.x=24;
	this.y=25;
	this.z=26;
};

// 既存インスタンス準備 
var t=new TestClassC();
t.init();
//console.log(t);
//console.log(t.getA());

// 実行内容 
var funx={
	dmy:()=>0, // 最初の実行項目は不利な結果が出るのでダミー 
	get:()=>t.a,
	call:()=>t.getA(),
	new_get:()=>{var u=new TestClassC(); u.init(); u.a},
	new_call:()=>{var u=new TestClassC(); u.init(); u.getA()},
};
// 計測結果を書き込むところ 
var rec={};

// 繰り返し実行時間計測 
var loop=Array(10000000);
for(var k in funx){
	var f=funx[k];
	var a=0;
	var bgn=new Date;
	for(var i of loop)a+=f();
	var end=new Date;
	rec[k]=end-bgn;
}

// インスタンス生成時間を抽出表示 
console.log(rec.new_get-rec.get);
console.log(rec.new_call-rec.call);
```

|  | Node.js | Deno |
|:--|--:|--:|
| get | 5.6 | 6.9 |
| call | 9.5 | 1.4 |

Denoで見違える結果になりました。
で、getよりcallの方が速い謎が増えた。
あと、 console.log(t) は一通り出てきてます。

# 実験: メソッドをアロー演算子で簡略表記方式

prototypeでアロー演算子使ってもうまくいかないんだよねぃ。
というわけで、 getA() の定義を init() 内に移してみた。

``` javascript:bench_class_lambda.js
// TestClassA 定義 
function TestClassA(){}
TestClassA.prototype.init=function(){
	this.a=1;
	this.getA=()=>this.a;
};

// TestClassB 定義 
function TestClassB(){}
Object.setPrototypeOf(TestClassB.prototype,TestClassA.prototype);
TestClassB.prototype.init_TestClassA=TestClassA.prototype.init;
TestClassB.prototype.init=function(){
	this.init_TestClassA();
	this.b=2;
	this.c=3;
	this.d=4;
	this.e=5;
	this.f=6;
	this.g=7;
	this.h=8;
	this.i=9;
};

// TestClassC 定義 
function TestClassC(){}
Object.setPrototypeOf(TestClassC.prototype,TestClassB.prototype);
TestClassC.prototype.init_TestClassB=TestClassB.prototype.init;
TestClassC.prototype.init=function(){
	this.init_TestClassB();
	this.j=10;
	this.k=11;
	this.l=12;
	this.m=13;
	this.n=14;
	this.o=15;
	this.p=16;
	this.q=17;
	this.r=18;
	this.s=19;
	this.t=20;
	this.u=21;
	this.v=22;
	this.w=23;
	this.x=24;
	this.y=25;
	this.z=26;
};

// 既存インスタンス準備 
var t=new TestClassC();
t.init();
//console.log(t);
//console.log(t.getA());

// 実行内容 
var funx={
	dmy:()=>0, // 最初の実行項目は不利な結果が出るのでダミー 
	get:()=>t.a,
	call:()=>t.getA(),
	new_get:()=>{var u=new TestClassC(); u.init(); u.a},
	new_call:()=>{var u=new TestClassC(); u.init(); u.getA()},
};
// 計測結果を書き込むところ 
var rec={};

// 繰り返し実行時間計測 
var loop=Array(10000000);
for(var k in funx){
	var f=funx[k];
	var a=0;
	var bgn=new Date;
	for(var i of loop)a+=f();
	var end=new Date;
	rec[k]=end-bgn;
}

// インスタンス生成時間を抽出表示 
console.log(rec.new_get-rec.get);
console.log(rec.new_call-rec.call);
```

|  | Node.js | Deno |
|:--|--:|--:|
| get | 10.8 | 12.9 |
| call | 712.6 | 556.8 |

init() の度に再定義しているわけだから重いのは必然だし、
console.log(t) で getA まで出てくる代物なのですが、
定義するだけで呼ばないならさほど大きなコストではないらしい。

# 実験: proto詰め込み方式

console.log(t) を気にしないでいいなら、こんな手法もあり。
プロパティの初期値もprototypeにぶっ込んでしまえば、そのぶんインスタンス生成コストも軽くなるわけで。

``` javascript:bench_class_fullproto.js
// TestClassA 定義 
function TestClassA(){};
TestClassA.prototype.a=1;
TestClassA.prototype.getA=function (){return this.a;}

// TestClassB 定義 
function TestClassB(){}
Object.setPrototypeOf(TestClassB.prototype,TestClassA.prototype);
TestClassB.prototype.b=2;
TestClassB.prototype.c=3;
TestClassB.prototype.d=4;
TestClassB.prototype.e=5;
TestClassB.prototype.f=6;
TestClassB.prototype.g=7;
TestClassB.prototype.h=8;
TestClassB.prototype.i=9;

// TestClassC 定義 
function TestClassC(){}
Object.setPrototypeOf(TestClassC.prototype,TestClassB.prototype);
TestClassC.prototype.j=10;
TestClassC.prototype.k=11;
TestClassC.prototype.l=12;
TestClassC.prototype.m=13;
TestClassC.prototype.n=14;
TestClassC.prototype.o=15;
TestClassC.prototype.p=16;
TestClassC.prototype.q=17;
TestClassC.prototype.r=18;
TestClassC.prototype.s=19;
TestClassC.prototype.t=20;
TestClassC.prototype.u=21;
TestClassC.prototype.v=22;
TestClassC.prototype.w=23;
TestClassC.prototype.x=24;
TestClassC.prototype.y=25;
TestClassC.prototype.z=26;

// 既存インスタンス準備 
var t=new TestClassC();
//console.log(t);
//console.log(t.getA());

// 実行内容 
var funx={
	dmy:()=>0, // 最初の実行項目は不利な結果が出るのでダミー 
	get:()=>t.a,
	call:()=>t.getA(),
	new_get:()=>new TestClassC().a,
	new_call:()=>new TestClassC().getA(),
};
// 計測結果を書き込むところ 
var rec={};

// 繰り返し実行時間計測 
var loop=Array(10000000);
for(var k in funx){
	var f=funx[k];
	var a=0;
	var bgn=new Date;
	for(var i of loop)a+=f();
	var end=new Date;
	rec[k]=end-bgn;
}

// インスタンス生成時間を抽出表示 
console.log(rec.new_get-rec.get);
console.log(rec.new_call-rec.call);
```

|  | Node.js | Deno |
|:--|--:|--:|
| get | -6 | 7.2 |
| call | 1.5 | 3.5 |

Denoの結果がいまいち。
どうもprototypeが重い御様子。
てゆかNode.js、マイナスってなんだよおい。


# 実験: Object.create() 方式

prototypeが重いなら、 Object.create() ならどうだ。

``` javascript:bench_class_create.js
// TestClassA 定義 
var TestClassA={a:1};
TestClassA.getA=function(){return this.a;};

// TestClassB 定義 
var TestClassB=Object.create(TestClassA);
TestClassB.b=2;
TestClassB.c=3;
TestClassB.d=4;
TestClassB.e=5;
TestClassB.f=6;
TestClassB.g=7;
TestClassB.h=8;
TestClassB.i=9;

// TestClassC 定義 
var TestClassC=Object.create(TestClassB);
TestClassC.j=10;
TestClassC.k=11;
TestClassC.l=12;
TestClassC.m=13;
TestClassC.n=14;
TestClassC.o=15;
TestClassC.p=16;
TestClassC.q=17;
TestClassC.r=18;
TestClassC.s=19;
TestClassC.t=20;
TestClassC.u=21;
TestClassC.v=22;
TestClassC.w=23;
TestClassC.x=24;
TestClassC.y=25;
TestClassC.z=26;

// 既存インスタンス準備 
var t=Object.create(TestClassC);
//console.log(t);
//console.log(t.getA());

// 実行内容 
var funx={
	dmy:()=>0, // 最初の実行項目は不利な結果が出るのでダミー 
	get:()=>t.a,
	call:()=>t.getA(),
	new_get:()=>Object.create(TestClassC).a,
	new_call:()=>Object.create(TestClassC).getA(),
};
// 計測結果を書き込むところ 
var rec={};

// 繰り返し実行時間計測 
var loop=Array(10000000);
for(var k in funx){
	var f=funx[k];
	var a=0;
	var bgn=new Date;
	for(var i of loop)a+=f();
	var end=new Date;
	rec[k]=end-bgn;
}

// インスタンス生成時間を抽出表示 
console.log(rec.new_get-rec.get);
console.log(rec.new_call-rec.call);
```

|  | Node.js | Deno |
|:--|--:|--:|
| get | -2.8 | 74 |
| call | 2.7 | 74.9 |

没。

# 実験: 空オブジェクトに詰め込み方式

おまけ。
既にprototype以外でfunction定義するとすごい重いって結論出ちゃったからあまり意味なくなっちゃったんだけど…

``` javascript:bench_class_pureobj.js
// TestClassA 定義 
function TestClassA(){
	var t={a:1};
	t.getA=()=>t.a;
	return t;
}

// TestClassB 定義 
function TestClassB(){
	var t=TestClassA();
	t.b=2;
	t.c=3;
	t.d=4;
	t.e=5;
	t.f=6;
	t.g=7;
	t.h=8;
	t.i=9;
	return t;
}

// TestClassC 定義 
function TestClassC(){
	var t=TestClassB();
	t.j=10;
	t.k=11;
	t.l=12;
	t.m=13;
	t.n=14;
	t.o=15;
	t.p=16;
	t.q=17;
	t.r=18;
	t.s=19;
	t.t=20;
	t.u=21;
	t.v=22;
	t.w=23;
	t.x=24;
	t.y=25;
	t.z=26;
	return t;
}

// 既存インスタンス準備 
var t=TestClassC();
//console.log(t);
//console.log(t.getA());

// 実行内容 
var funx={
	dmy:()=>0, // 最初の実行項目は不利な結果が出るのでダミー 
	get:()=>t.a,
	call:()=>t.getA(),
	new_get:()=>TestClassC().a,
	new_call:()=>TestClassC().getA(),
};
// 計測結果を書き込むところ 
var rec={};

// 繰り返し実行時間計測 
var loop=Array(10000000);
for(var k in funx){
	var f=funx[k];
	var a=0;
	var bgn=new Date;
	for(var i of loop)a+=f();
	var end=new Date;
	rec[k]=end-bgn;
}

// インスタンス生成時間を抽出表示 
console.log(rec.new_get-rec.get);
console.log(rec.new_call-rec.call);
```

|  | Node.js | Deno |
|:--|--:|--:|
| get | 7.4 | 8.4 |
| call | 1173.1 | 803.3 |

ということで。
因みに、プロパティ定義で横着して
Object.assign(TestClassA(),{b:2,...}) 
みたいな書き方すると、さらに悲惨な結果になります。
JavaScriptでのオブジェクト合成って、 [何やっても重い](https://qiita.com/Yggdrasil-Leaves/items/711b4f139a844fea889c) ので困ったもんだ。

# 結論

邪道実装でいい案件なら、proto詰め込み方式を推し進めたいところ。
あと、やはりDenoはパフォーマンス改善まで様子見。