Node.js の EventEmitter のコードを読んで、速度最適化されたコードについて考える
大都会岡山 Advent Calendar 2012 20日目のエントリーです。
前回の記事は @DAI199 さんによる 中国で流行っていること、感じたこと(大都会岡山 AdventCalender2012 19日目) - tagamidaiki.com でした。
20 日目のエントリーは、大都会岡山で生まれ、大都会岡山のコミュニティの方々を尊敬し、そして札幌で暮らしている(!) @tricknotes が書かせていただきます。
ぼくの記事ではタイトルの通り、速度最適化された js コードについて見ていきたいと思います。
はじめに
「その書き方だと遅いから」「こっちの方が速いよ」なんていう言葉をちょくちょく耳にする機会があります。
でもそれって本当でしょうか?
コードの実行速度は処理系の実装と密に関係していて、パフォーマンス計測なしには語ることができません。
また、そのコードの実行のされ方によっても実行速度は大きく変わってくるでしょう。
(たとえば、実行時のホットスポットがどこになるかというのは、引数やオブジェクトの状態などに左右される場合があります。)
そこで今回は、 Node.js の EventEmitter を読んで、 JavaScript の速度最適化について考えてみることにしましょう。
Node.js を選んだ理由
Node.js はイベント駆動の非同期スタイルを強制していることで知られています。そして、このイベント駆動のインターフェースを提供しているモジュールが EventEmitter です。
Stream や http.Server など多くの主要なモジュールがこの EventEmitter を継承することで、イベント駆動のインターフェースをユーザに提供しています。
(参考)
ということは、 EventEmitter 内でパフォーマンスの悪い箇所があれば、Node.js 全体のパフォーマンスに大きな影響を与えることになります。
実際に、この EventEmitter の中では多くの速度最適化が施されており、これが Node.js 全体の実行速度を維持するための重要なポイントとなっています。
(イベント名に __proto__
など、特定の名前が利用できないという不具合がありますが、それよりもスピードの方が重要であるという方針とのことです joyent/node#4366)
ではさっそく、この EventEmitter 中からいくつかコードを例に挙げながら、実際に速度を計測して本当に速度最適化されていることを確認していきましょう。
EventEmitter は events.js の中で定義されています。
本エントリーでの対象バージョンは Node.js 0.9.4-pre (9f4c0988) とします。
$ node -v v0.9.4-pre $ node > process.versions { http_parser: '1.0', node: '0.9.4-pre', v8: '3.13.7.4', ares: '1.9.0-DEV', uv: '0.9', zlib: '1.2.3', openssl: '1.0.1c' }
最適化コードについて
○ Function#apply より Function#call
Function オブジェクトを、任意のオブジェクトをコンテキストとして実行するためのメソッドとして Function#apply と Function#call があります。
多くの処理系の場合と同じく、Node.js の場合もこの両者の間には大きな速度の差あるようです。
そのため、極力 Function#apply を使わないようなコードになっています。
だいぶ濃い感じになっていますね。
普通に書くと、以下のように1行で済むはずです。
handler.apply(this, Array.prototype.slice.call(arguments, 1));
実際、両者の間にどれくらいの速度差があるのか測定してみましょう。
// Function#apply と Function#call の比較 var count = 10000000; var now, i; var obj = {}; var key = 'key'; var fn = function() {}; now = Date.now(); for (i = 0; i < count; i++) { fn.apply(this, [1]); } console.log('* apply: %d ms', Date.now() - now); now = Date.now(); for (i = 0; i < count; i++) { fn.call(this, 1); } console.log('* call: %d ms', Date.now() - now);
* apply: 519 ms * call: 160 ms
たしかに Function#apply の方が遅いようですね。
○ delete より null
オブジェクトの特定のキーに紐付く値を削除する場合、値だけを削除する方法(null や undefined を代入する方法)とキーごと削除する方法(delete キーワードを使う方法)があります。Object#hasOwnProperty などを利用せず、値の truthy/falsy のチェックだけを行うのであれば、この両者の間で大きな動作の違いはありません。
ただ、Node.js の場合は前者の方が圧倒的に高速に動作します。
そのため、不要になったオブジェクトを破棄する際には、delete を利用せず null を代入するようになっています。
EventEmitter の中では、 EventEmitter#removeListener と EventEmitter#removeAllListeners の中で使われています。
// delete と null を代入の比較 var count = 1000000; var now, i; var obj = {}; var key = 'key'; now = Date.now(); for (i = 0; i < count; i++) { obj[key] = 1; obj[key] = null; } console.log('* obj[key] = null: %d ms', Date.now() - now); now = Date.now(); for (i = 0; i < count; i++) { obj[key] = 1; delete obj[key]; } console.log('* delete obj[key]: %d ms', Date.now() - now);
* obj[key] = null: 21 ms * delete obj[key]: 405 ms
速度が処理系に依存することの参考までに、このベンチマークを FireFox 16.0.2 (Max OS X) で実行してみました。
すると、以下のような結果となりました。
* obj[key] = null: 5520 ms * delete obj[key]: 5857 ms
6%程度の差なのでそこまで大きな差ではありません。
この結果から、速度は処理系に依存していることがわかります。
○ Array#slice より for 文 & 代入
配列(もしくは arguments) を部分的に切り出したい場合、Array#slice を利用するのが簡単です。
ただ、Node.js では Array#slice を呼ばないように、 Array を要素数指定で初期化して、ひとつづつ値を詰めている箇所があります。
- https://github.com/joyent/node/blob/9f4c0988c37b9df60e45c26c25c91e45757d8f62/lib/events.js#L102-L103
ここを簡単に記述するなら以下のようになります。
Array.prototype.slice.call(arguments, 1);
これについてはどの程度パフォーマンスに影響が出ているのでしょうか?
// Array#slice(1) と new Array() + for についての比較 var count = 10000000; var now, i; var obj = {}; var key = 'key'; var array = ['type', 1, 2, 3 /* any arguments */]; now = Date.now(); for (i = 0; i < count; i++) { array.slice(1); } console.log('* Array#slice(1): %d ms', Date.now() - now); now = Date.now(); for (i = 0; i < count; i++) { var l = array.length; var _array = new Array(); for (var j = 1; j < l; j++) { _array[j - 1] = array[j]; } } console.log('* new Array(): %d ms', Date.now() - now); now = Date.now(); for (i = 0; i < count; i++) { var l = array.length; var _array = new Array(l - 1); for (var j = 1; j < l; j++) { _array[j - 1] = array[j]; } } console.log('* new Array(length): %d ms', Date.now() - now);
* Array#slice(1): 837 ms * new Array(): 388 ms * new Array(length): 267 ms
以上のことより、Array#slice を使った場合とそうでない場合では、速度差があることがわかります。
補足
ここまででは書いていませんでしたが、部分的にコードを取り出しただけのベンチマークでは不十分なケースもあります。
コードの書き方だけではなく、文脈に沿って最適化が行われる場合があるので、ちゃんとプロダクションコードに対してベンチーマークをとって全体で動かしてみるまで、速度最適化されたかどうかは判定できません。
(ということを @koichik さんに教えてもらいました。勉強になります!)
@tricknotes 手抜きしてこちらで。すでにマージされた GitHub の PR (4393) ですが、ベンチマークを載せるなら修正前後の emit() 呼び出しで比較すべきだったと思います。最適化の効き具合による影響があるためです。(続く)
2012-12-10 23:03:59 via TweetDeck to @tricknotes
@tricknotes (続き) 実際、昨年 arguments を使わないようにしたところ、emit() のパフォーマンスは劣化しました。現在の V8 とはバージョンが違うので今では当てはまらないかもしれませんが、あのベンチマークでは確認にならないと思います。
2012-12-10 23:05:25 via TweetDeck to @tricknotes
結論
ここまで見てきてみなさまお分かりでしょうが、速度に最適化することと人間に最適化することが共存できるとは限りません。速度に最適化し過ぎてリーダブルではないコードになってしまうと、メンテナンス性を著しく下げてしまうことでしょう。しかし、ここまでで見てきた EventEmitter の例のように、可読性を捨ててでも速度に最適化すべき場面というのも確かに存在します。
もちろんその場合には、プロジェクトの性質によってどこまで速度最適化をするかということを判断する必要があると思います。
また、本エントリーの内容は、Node.js 0.9.4-pre (9f4c0988) での挙動です。
今後、同じコードで同じ最適化の効果が得られると保証されているわけではありません。
(関連)
みなさまのアプリケーションでも、もしパフォーマンスが問題であると感じたときは、ホットスポットがどこにあるか計測しつつ、どこまで可読性を犠牲にするかということを考えながらチューニングしていくことをオススメします。
まとめ
以上でぼくの記事は終わりとさせていただきます。
次回の大都会岡山 Advent Calendar 2012 は sutorada さんです。
引き続き、大都会岡山 Advent Calendar 2012 をお楽しみください:-)
SaCSS vol.40 で「ブラウザサイド MVC 入門」というタイトルで発表してきました
ブラウザの中での MVC を紹介する際に TODO アプリがよく例にあげられるかと思いますが、今回ぼくの発表ではその TODO アプリの手前までを丁寧に説明しました。
「なんでMVCが必要とされているのか」であったり、「解決する問題領域は何か」といったあたりを詳しく見ていく、という内容です。
概要はこんな感じ: (SaCSS vol.40 より)
ブラウザサイド MVC 入門画面遷移を行わず、1枚の html 上で対話的に操作するような web アプリケーションを目にする機会が増えてきました。
そんなアプリケーションを開発する際、jQuery 主体だったこれまでの開発スタイルだと画面表示と機能が切り離せなくなってしまい、変更に弱い作りになってしまうことになるでしょう。
この問題に対処するにはどうすればよいでしょう?
そんなときは先人の知恵を参考に、その解決策を探してみるのはいかがでしょうか。
ユーザからのインタラクションを受け付けるアプリケーションを上手く構築していく際のやり方として、MVC パターンが広く知られています。
本セッションでは、ブラウザ上での JavaScript の MVC の原点を辿り、その実装のひとつである Backbone.js を例にとって、整理されたコードについて考えてみたいと思います。
ウェブデザイナーさんが多かったのですが、だいぶ置いてけぼりにしてしまった感があって大変申し訳なかったなぁ、というのが反省:-<
今回、声をかけてくれた @h2ham に感謝!
岡山Ruby会議01に参加してきました
岡山Ruby会議01に参加してきました。
「Ruby の世界に入ってみよう」というテーマで、これからRubyを始めるひとへのメッセージが詰まった会議でした。
とは言っても、初心者向けな内容ではなく、Ruby を使っている中での実体験の話や、開発自体の話が多くて、とても面白かったです。
そして、ぼくも LT で話しさせていただきました
capybara を紹介する体で、ちょっと変わった使い方も試してみました。
rails 以外のwebアプリケーションにスペックを書いたり、RSpec で実行した結果を画面キャプチャ付きで Excel ファイルに出力したり、という内容です。
最後に、スタッフのみなさま、ありがとうございました&お疲れさまでした。
次回の岡山Ruby会議もまた参加したいなぁ。
NotHub で自分に関する通知を受け取れるようになりました
NotHub 0.3.0 をリリースしました!
このバージョンからは、 自分(github.com にログインしているアカウント)に関する通知が受け取れるようになりました。
例えば、こんな通知を受け取れます:
- 誰かがあなたを follow した
- 誰かがあなたのリポジトリを watch した
- 誰かがあなたのリポジトリに pull request / issue を送った
- 誰かがあなたの pull request にコメントつけた
例えば、こんな楽しみ方ができます:
- あなたの送った pull request へのフィードバックがあれば、いち早く知ることができます。
- 新しいリポジトリを作った時に、みんなに watch されるのを眺めてニヤニヤすることができます。
また、現在ログインしているアカウント名を参照するために、NotHub から github.com にアクセスするようになりました。
ここで拡張機能の権限を変更したため、NotHub を更新すると NotHub 自体が無効状態になります。
権限設定を確認の上、再度有効にしてください。
この機能はデフォルトでは off になっているので、
使ってみたい方は NotHub を更新した後、ここのチェックボックスを on にしてください:-)
まだ使っていない方はこちらからインストールしてみてください!
ブラウザ用に書かれた mocha のテストを Node.js で動作させる mocha-ci-driver を作ってみました
JavaScript のテストを作成する際、動作環境を意識したコードを書くことを手間に感じる方は多いかと思います。
そこで今回は、ブラウザ用に作成した JavaScript のテストコードを、 Node.js を利用した CI 環境でも同じように動作させることができるツールとして、 mocha-ci-driver を作ってみたのでご紹介したいと思います。
* ブラウザでのテスト
本来、ブラウザ用に書かれたテストは基本的にはブラウザでしか動作しません。
受け入れレベルのテストを selenium などを利用して動作させるというのはよくある手段ですが、モデルのみのテストだとなかなかそうもいきません。
そのため、JavaScript のテストをすべて CI に組み込んで動作させることは困難かと思います。
ひとつのアプローチとして、ブラウザでもサーバ(今回は Node.js を対象としています)でも動作するようなコードに書き換えるというのも一つの手段かもしれません。
ロジックだけのモデル層のテストなら互換性を考慮するのも可能でしょう。
(function(global) { // code })( 'undefined' === typeof exports ? window : module.exports );
ただ、外部のライブラリや DOM に依存している部分についてはなかなか素直にはいきません。
例えば、 underscore や jQuery を利用していたら、その部分をブラウザ/サーバ用に差し替えるようなコードが必要になってくるでしょう。
(function(global, _) { // code })( // 環境依存を解消するためのコード(本質的ではない) 'undefined' === typeof window ? module.exports : window, 'undefined' === typeof _ ? require('underscore') : _ );
この対応というのは、本質的なコードではない上にテストの信頼性も損なってしまいます。
またこの問題を解消できたとしても、DOM に依存するコードは単独でテストをしづらいという課題が残ります。
ブラウザで動作しているテストコードを、特別な変更無しで CI 環境でも動作させることができたら素敵ですよね。
というわけで、作ってみました。
* mocha-ci-driver を使うと
mocha-ci-driver は mocha で書かれているブラウザ用のテストを、Node.js 上でも動作させるためのドライバです。
これを使うと、ブラウザ用に書かれたテストを CI に組み込めます。
なので、普段の開発では CI で動作させておいて、ブラウザ互換を確認したいときは各ブラウザでテストを実行することができるようになります。
Node.js 用の設定はこんな感じです。
1. まず、ドライバを実行するためのファイルを追加します。
(値は各環境によって適宜変更してください。)
// ./test/driver.js var Driver = require('mocha-ci-driver').Driver , basedir = __dirname + '/../' , port = 8080 , testHtml = '/test/index.html' , driver = new Driver(basedir, port) driver.run(testHtml);
2. 次に、普段利用しているテスト用の html を一部修正し、
html の中でなくドライバ側でテストを実行するよう設定します。
(mocha はテストを実行した後はテスト内容を捨ててしまうので、この設定が無いと、ドライバ側で実行すべきテストを取得できなくなってしまいます。)
(before)
<script> $(function () { mocha.run(); }); </script>
(after)
<script> $(function () { // Node.js で実行時にはこのタイミングでテストを実行しない if (!/Node.js/.test(navigator.appName)) { mocha.run(); } }); </script>
3. この設定が完了すれば、あとは Node.js で実行するだけです。
$ node ./test/driver.js
* mocha の対応バージョン
ただ、この mocha-ci-driver が対応しているのは、 visionmedia/mocha@a186b8dba1 以降になります。
2012.03.04 現在、 tag は切られていないので、 mocha-ci-driver を利用するためにはリポジトリから mocha を取得してきて make する必要があります。
$ git clone git://github.com/visionmedia/mocha.git mocha
$ cd mocha
$ make clean && make
これで mocha.js がビルドされるので、この mocha.js をテスト用の html で利用するようにしてください。
というわけで、 ブラウザでもサーバでも動作するテストに興味がある方は mocha-ci-driver 試してみてください:-)
WebSocket の動作確認に wscat が便利すぎる件
WebSocket を利用したアプリケーションを作る際に、動作確認が煩雑な場合があります。
サーバ側とクライアント側をどちらも実装する必要があって、「ちょっとこの部分だけ動かしてみたいなぁ」っていうときに、簡単に試す方法があると便利ですよね!
そんなときにおすすめなのが、 wscat です。
wscat は、コマンドラインで利用できる WebSocket のサーバ/クライアントで、ws に同梱されています。
ws とは、 Node.js 上で WebSocket を使うためのモジュールで、Socket.IO やengine.io の内部でも利用されている今注目のプロダクトです。
今回は、この wscat の使い方をご紹介します。
対象バージョン
- ws (0.4.7)
インストール
Node.js のモジュールなので、 npm でインストールするのが簡単です。
$ npm install -g ws
利用
完了したら、WebSocket サーバに接続します。
(localhost:3000 でサーバが起動している場合)
$ wscat -c ws://localhost:3000
と入力すると、localhost:3000 に接続した状態で wscat が起動します。
connected (press CTRL+C to quit) >
この状態でメッセージを入力して Enter を押すと、入力した内容が接続先のサーバに送信されます。
また、サーバからクライアントへメッセージを送信した場合は、今開いているターミナルの標準出力にメッセージが表示されます。
サーバとして起動する場合はこちら
(3000 番ポートで listen する場合)
$ wscat -l 3000
クライアントから受け取ったメッセージを、そのまま標準出力に表示するようになっています。
手元に WebSocket のサーバもクライアントも無い場合でも、ターミナルを2つ開いて wscat をサーバとクライアントで動かせば、動作を確認できます。
ちょっとしたデバッグなどに手軽に利用できるので、 WebSocket アプリを開発する際にオススメにツールです:-)
ソースはこちら: https://github.com/einaros/ws
※ もちろん、何度も確認する必要があるならきちんとテストコードを書いた方が良いです
Ruby Sapporo Night vol.14 に参加してきました
Ruby Sapporo Night vol.14 に参加して発表してきました。
- Ruby札幌 | Ruby Sapporo Night vol. 14 『Ruby札幌 × Sapporo.js』
- Sapporo.js | Ruby Sapporo Night vol. 14 『Ruby札幌 × Sapporo.js』
@snoozer05 に声をかけていただいたので、喜んで参加しました。
そんな @snoozer05 の発表資料はこちら: Ruby Sapporo Night vol.14
で、ぼくの資料はこちらです。
クライアントサイドの MVC って、なんやろなぁ。
大事なことってなんだろう?
ってことについて、今の自分が考えていることをお話したつもりです。
また、今回の発表で使ったデモ(Task List)はこちらにあります。
このデモでは、クライアントサイドMVC に特化したフレームワークはあえて使わないで、どう設計すれば上手くいくかを試行しながら作ってみました。
結果としては Model にも View にもユニットテストが書けるような作りになっています。
これについては、「もっとこうしたら良いのでは」とか「テストをもっと良くできる」などある方がいれば、ぜひぜひ GitHub の方でコメント or issue or pull request いただければと思います。
Ruby札幌のみなさま、参加されたみなさま、ありがとうございました!