thisもnewもprototypeも使わない擬似クラス設計 (評価編)
thisもnewもprototypeも使わない擬似クラス設計 - DebugIto’s diaryではクロージャを使ったオブジェクトの作り方について考えてみたが、今回はそれをnode.jsでベンチマークしてみた。
評価環境。
OS | Xubuntu Linux 12.04 32bit desktop |
CPU | AMD Athlon II X2 235e |
node.jsバージョン | 0.8.15 |
まず、クロージャを使ったクラス定義とプロトタイプによるクラス定義を書く。その他、ベンチマーク用ユーティリティ関数も含めてclasses.jsとして保存。
/////////////////// 汎用ユーティリティ function defined(v) { return (v !== undefined && v !== null) } // ** objからキー _init に格納されるオブジェクトを引き抜き、 // ** objの残りメンバで破壊的に拡張して返す。 // ** objにキー _init が無い場合は空オブジェクトを新規作成して拡張 function build(obj) { var base; var key; if(defined(obj._init)) { base = obj._init; delete obj._init; }else { base = {}; } for(key in obj) { if(!obj.hasOwnProperty(key)) continue; base[key] = obj[key]; } return base; } /////////////////// クロージャによるクラス定義 var myFigure = build({ default_x: 0, default_y: 0, _init: function(name, init_x, init_y) { var x = defined(init_x) ? init_x : myFigure.default_x; var y = defined(init_y) ? init_y : myFigure.default_y; var self = { toString: function() { return self.getName() + ": x = " + self.getX() + ', y = ' + self.getY(); }, print: function() { console.log(self.toString()); }, getName: function() { return name }, getX: function() { return x }, getY: function() { return y } } return self; }, }); var myCircle = build({ SUPER: myFigure, default_radius: 10, _init: function(name, init_x, init_y, radius) { var self = myCircle.SUPER(name, init_x, init_y); var super_toString = self.toString; if(!defined(radius)) radius = myCircle.default_radius; build({ _init: self, getRadius: function() { return radius }, toString: function() { return super_toString() + ", R = " + self.getRadius(); }, }); return self; } }); /////////////////// prototypeによるクラス定義 var Figure = build({ default_x: 0, default_y: 0, _init: function(name, init_x, init_y) { this.name = name; this.x = defined(init_x) ? init_x : Figure.default_x; this.y = defined(init_y) ? init_y : Figure.default_y; }, prototype: { toString: function() { return this.getName() + ": x = " + this.getX() + ', y = ' + this.getY(); }, print: function() { console.log(this.toString()); }, getName: function() { return this.name }, getX: function() { return this.x }, getY: function() { return this.y }, } }); var Circle = build({ SUPER: Figure, default_radius: 10, _init: function(name, init_x, init_y, radius) { Circle.SUPER.apply(this, [name, init_x, init_y]); this.radius = defined(radius) ? radius : Circle.default_radius; }, prototype: build({ _init: new Figure(""), getRadius: function() { return this.radius }, toString: function() { return Circle.SUPER.prototype.toString.apply(this) + ", R = " + this.getRadius(); } }) }); exports.myFigure = myFigure; exports.myCircle = myCircle; exports.Figure = Figure; exports.Circle = Circle; ////////////////////// ベンチマーク用ユーティリティ var REPEAT_COUNT = 100000; exports.time = function(func) { var start = process.hrtime(); var i; for(i = 0 ; i < REPEAT_COUNT ; i++) { func(); } return process.hrtime(start); }; exports.printResults = function(times, memory) { var key, val; for(key in times) { val = times[key]; console.log(key + ": " + (val[0] * 1000 + val[1] / 1000000.0) + "ms"); } for(key in memory) { val = memory[key]; console.log(key + ": " + (val / 1000.0) + "kB"); } }
まず、クロージャを使った場合。
var classes = require('./classes.js'); var myFigure = classes.myFigure; var myCircle = classes.myCircle; var figures = []; var circles = []; var times = {}; times["Construct figures"] = classes.time(function() { figures.push(myFigure("a", 10, 10)); }); times["Construct circles"] = classes.time(function() { circles.push(myCircle("A", 10, 10, 10)); }); times["getX() on figures"] = classes.time(function() { var x = figures[0].getX(); }); times["getX() on circles"] = classes.time(function() { var x = circles[0].getX(); }); var memory = process.memoryUsage(); classes.printResults(times, memory);
次に、プロトタイプによる場合。
var classes = require('./classes.js'); var Figure = classes.Figure; var Circle = classes.Circle; var figures = []; var circles = []; var times = {}; times["Construct figures"] = classes.time(function() { figures.push(new Figure("a", 10, 10)); }); times["Construct circles"] = classes.time(function() { circles.push(new Circle("A", 10, 10, 10)); }); times["getX() on figures"] = classes.time(function() { var x = figures[0].getX(); }); times["getX() on circles"] = classes.time(function() { var x = circles[0].getX(); }); var memory = process.memoryUsage(); classes.printResults(times, memory);
classes.time()関数は与えられた関数を10,000回繰り返し実行してかかった時間を返す。
結果。
項目 | closure | prototype | closure/prototype(%) |
Construct circles | 631.57ms | 36.98ms | 1707.92% |
Construct figures | 176.58ms | 17.78ms | 992.89% |
getX() on circles | 2.28ms | 2.31ms | 98.47% |
getX() on figures | 2.70ms | 2.42ms | 111.45% |
heapTotal | 70201.22kB | 17583.23kB | 399.25% |
heapUsed | 65760.93kB | 7791.26kB | 844.03% |
rss | 76746.75kB | 20951.04kB | 366.31% |
なるほど。これはヒドい。
プロトタイプ型に比べ、クロージャ型のコンストラクタは実行に10倍近い時間がかかる。継承が深くなればもっと差は大きくなるだろう。消費メモリ量もクロージャ型はざっと4倍といったところ。
クロージャ型も工夫すればもっと効率良くなるかもしれないが、まあ素直にプロトタイプ使っとけということか。