はじめまして!
2013年度新卒入社で現在、スクールファンファーレのサーバサイドエンジニアを担当しているsuguruです。
スクファンの事前登録はコチラ!

JavaScriptの非同期処理を行うときに、Async.jsというとても便利なライブラリがあります。
しかしAsync.jsはパフォーマンスの面ではあまりチューニングされていなく、改善する余地があります。
そこでパフォーマンスを改善したNeo-Async.jsについて紹介させていただきます。

 

Async.js

Neo-Async.jsに入る前に、Async.jsを紹介します。
Async.jsとはJavaScriptの非同期処理を直線的に書くことができるライブラリです。
直線的に書くことで可読性が高く、バグが起こりにくいコードを実現できます。
hoge(function(err, result) {
 fuga(result, function(err, result) {
   piyo(result, function(err, result) {
     console.log(result);
   });
 });
});

// ↓

async.waterfall([
 function (next) {
   hoge(next);
 },
 function (result, next) {
   fuga(result, next);
 },
 function (result, next) {
   piyo(result, next);
 }
], function(err, result) {
 console.log(result);
});
また並列処理や他にも便利なメソッドを多数揃えています。

Neo-Async.js

Neo-Async.jsとはAsync.jsと完全に互換性があり、 より高速でより利便性が高いライブラリです。
まずは処理速度について紹介します。以下のバージョンで測定します。
  • Async v0.9.0
  • Neo-Async v0.4.8

フロントエンドの速度比較

フロントエンドのベンチマーク計測にjsPerfを利用しました。 実行環境やプログラムにより速度が全く異なってくるので、ほんの一例です。
計測環境は以下の3つの環境です。
  • Chrome 40.0.2214
  • FireFox 34.0
  • Safari 8.0.2

結果

jsperf_waterfall図1: waterfallの実行例
functionChromeFireFoxSafariurl
waterfall2.182.202.36http://jsperf.com/async-waterfall/7
series1.501.311.10http://jsperf.com/async-series/8
parallel15.6710.175.01http://jsperf.com/async-parallel/5
parallelLimit1.351.411.11http://jsperf.com/async-parallel-limit/2

waterfallでは約2倍の速度が出ており、フロントサイドではパフォーマンスの改善が期待できます。

サーバサイドの速度比較

サーバサイドのベンチマーク計測には簡易計測ツールを作って調べました。
ツールの仕様は以下の通りです。
  • n回試行
  • 毎回順番がランダム
  • 毎回gcを走らせる
  • n回の平均速度[μs]を計測

demo.js

var comparator = require('func-comparator'); // 今回作った計測ツール
var _ = require('lodash');
var async = require('async');
var neo_async = require('neo-async');

var count = 10; // parallelのタスク数
var times = 1000; // 試行回数
var array = _.shuffle(_.times(count));
var tasks = _.map(array, function(n) {
 return function(next) {
   next(null, n);
 };
});

// ここのfuncsを交互ではなく毎回ランダムで実行する
var funcs = {
 'async': function(callback) {
   async.parallel(tasks, callback);
 },
 'neo-async': function(callback) {
   neo_async.parallel(tasks, callback);
 }
};

comparator
.set(funcs)
.option({
 async: true,
 times: times
})
.start()
.result(console.log);

実行

task数10で1000回実行した平均速度を比較します。
実行環境は以下の通りです。
  • node v0.10.35
  • iojs v1.0.2
$ node --expose_gc demo.js // gcを使わずに実行も可能
$ iojs --expose_gc demo.js

結果

数値はn回の平均速度の比(Async/Neo-Async)になります。

functionnodeiojs
waterfall3.4712.05
series1.986.38
parallel2.948.94
paralellLimit2.886.13

node・iojsでもレスポンス改善が期待できます。

waterfallの速度比較

taskのサイズによっても速度が大きく変わってくるため、task数の変化による速度変化を調べます。
ツールの仕様は以下の通りです。
  • task数がlowerからinterval間隔でupperまで実行
  • 毎回順番がランダム
  • 毎回gcを走らせる
  • n回の平均速度[μs]を計測

demo2.js

var statistic = require('func-comparator').statistic;
var _ = require('lodash');
var async = require('async');
var neo_async = require('neo-async');

// サンプリング回数
var times = 100;
var create = function(count) {
 // countはtask数
 var array = _.shuffle(_.times(count));
 var tasks = _.map(array, function(n, i) {
   if (i === 0) {
     return function(next) {
       next(null, n);
     };
   }
   return function(total, next) {
     next(null, total + n);
   };
 });
 var funcs = {
   'async': function(callback) {
     async.waterfall(tasks, callback);
   },
   'neo-async': function(callback) {
     neo_async.waterfall(tasks, callback);
   }
 };
 return funcs;
};

statistic
.create(create)
.option({
 async: true,
 times: times,
 count: {
   lower: 10,
   upper: 1000,
   interval: 10
 }
})
.start()
.result(console.log)
.csv('waterfall_' + _.now());

実行

task数10~1000、間隔は10刻みで、毎回の試行回数は100回です。 実行環境は以下の通りです。
  • node v0.10.35
  • iojs v1.0.2
$ node --expose_gc demo2.js
$ iojs --expose_gc demo2.js

結果

処理結果は以下の図になります。x軸はtask数、y軸は平均処理時間[μs]です。

iojs_waterfall
図2: nodeの速度比較
node_waterfall図3: iojsの速度比較
task数が大きくなるにつれ速度差・速度比が大きくなってきます。 Neo-Asyncではtask数が増えても高パフォーマンスが期待できます。

利便性の向上

underscoreやLo-DashではObjectをArrayのforEachのように実行することが当たり前のようにサポートされていますが、 Asyncではサポートされていません。
Neo-Asyncではほとんどのfunctionでサポートしており、これは何かと便利な機能です。
var object = {
 HOGE: 'hoge',
 FUGA: 'fuga',
 PIYO: 'piyo'
};

async.each(Object.keys(object), function(key, done) {
 var str = object[key];
 /* 処理 */
 console.log(str);
 done();
}, callback);

neo_async.each(object, function(str, done) {
 /* 処理 */
 console.log(str);
 done();
}, callback);

終わりに

Asyncの実装はとてもきれいで勉強になりますので、ぜひ読んでいただきたいです。
Neo-Asyncはとてもコードは冗長ですが、高パフォーマンスを実現することができました。
これからもより快適なゲームが作れるように努めて行きたいと思います。
スクファンもよろしくお願いします!(`・ω・´)