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");
    }
}

classes.jsを使ってベンチマークスクリプトを書く。

まず、クロージャを使った場合。

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倍といったところ。

クロージャ型も工夫すればもっと効率良くなるかもしれないが、まあ素直にプロトタイプ使っとけということか。