Node.js の process.nextTick でスタックオーバーフロー!

今 JavaScript がアツいですね!Node.js や MongoDB なんかのおかげでしょうか?AWS も CloudFormation だの IAM だののコンフィグを見る限り JSON を活用しているようですし、JavaScript はやらなきゃ損損!

と勢い込んでますが、Node.js というやつは非同期という特徴を押し出しててちょっと取っ付きにくいような気がします。長い処理をする時は、時々 process.nextTick を呼び出して他の処理に制御を移すのがマナーらしいんですが…。

function add(a, b) { return a + b; }

/* Array.reduce の非同期版のようなものです。
 */
Array.prototype.myReduce = function(f, cb, n) {
    if (typeof n == "undefined") {
        n = 1;
    }

    // this (配列) をコピーします。
    ary = JSON.parse(JSON.stringify(this));

    // ary に対して n 回分 f を適用し、残りは process.nextTick で次回に
    // 持ち越します。
    function subMyReduce() {
        for (var i = 0; i < n; i++) {
            if (ary.length == 1) {
                return cb(null, ary[0]);
            }

            var a = ary.shift();
            var b = ary.shift();
            ary.push(f(a, b));
        }

        process.nextTick(subMyReduce);
    }

    subMyReduce();
}

var xs = [];
for (var i = 0; i < 30000; i++) {
    xs.push(Math.floor(Math.random() * 5));
}

xs.slice(0, 1000).myReduce(add, function(err, result) {
    console.log(result);
});

上の myReduce, 内部ループの n が 1 の時、対象の配列の要素数が 1,000 くらいなら問題ないですが、それより多いとスタックオーバーフロー (RangeError: Maximum call stack size exceeded) になってしまいます。

どうしたものかと Google 検索してみると、process.nextTick の代わりに setTimeout を使うといいようです:

        setTimeout(subMyReduce, 0);

が、劇的に遅くなってしまうのが玉に瑕。
前述の条件で process.nextTick を使うと実行時間は 15 ミリ秒くらいですが、setTimeout にすると一気に 2,000 ミリ秒くらいに…。

それならばと process.nextTick と setTimeout を組み合わせてみました:

Array.prototype.myReduce = function(f, cb, n) {
    if (typeof n == "undefined") {
        n = 1;
    }   

    // this (配列) をコピーします。
    ary = JSON.parse(JSON.stringify(this));

    var j = 0;

    // ary に対して n 回分 f を適用し、残りは process.nextTick で次回に
    // 持ち越します。
    function subMyReduce() {
        for (var i = 0; i < n; i++) {
            if (ary.length == 1) {
                return cb(null, ary[0]);
            }

            var a = ary.shift();
            var b = ary.shift();
            ary.push(f(a, b));
        }

        if (++j < 1000) {
            process.nextTick(subMyReduce);
        } else {
            j = 0;
            setTimeout(subMyReduce, 0);
        }
    }

    subMyReduce();
}

これなら要素数 80,000 でも 200 ミリ秒くらいで実行完了しました。
要素数がこれより大きいとハングアップするようですが、また別の問題かもしれません。

ともかく、気配りするのも大変です!

(コウヅ)

広告