Node.js-v0.10.28でDate.now()が爆速になった
1. Node-v0.10.28とNode-v0.11.13のリリース
先週末、安定版のNode-v0.10.28と開発版のNode-v0.11.13がリリースされました。*1
安定版の0.10系は、基本バグフィックスなどが中心のリリースですが、今回
deps: make v8 use CLOCK_REALTIME_COARSE
のような修正が入りました。Node-v0.10のV8でLinux向けにNode固有の性能向上パッチが適応されたようです。
CLOCK_REALTIME_COARSEとはどのようなものでしょうか? RedHatのドキュメント「15.2.1. CLOCK_MONOTONIC_COARSE と CLOCK_REALTIME_COARSE」では、
カーネルへのコンテキストスイッチを避けるため、そして結果的にクロックの読み取りを迅速に行うために、CLOCK_MONOTONIC_COARSE および CLOCK_REALTIME_COARSE POSIX クロックへのサポートが VDSO ライブラリ関数の形式で可能になりました。
と説明されています。VDSO (Virtual Dynamically linked Shared Objects)とは、カーネルスペースの処理をユーザスペースで代用実行できるライブラリ機能で、システムコールの呼び出しにかかる負荷を大幅に低減できる仕組みです。
今回のパッチは、 Date.now() の時刻取得で利用されていたシステムコール(gettimeofday(2))を、VDSOに対応したシステムコール(CLOCK_REALTIME_COARSEを利用した clock_gettime(2))に置き換えて、Date.now()の高速化を図ったものです。これを利用できる条件は、今回のパッチでは
- Linux (kernel 2.6.32以上)であること。
- CLOCK_REALTIME_COARSEの時間解像度が 1 msec 以下であること。
が必要で、この2つ目の条件がLinuxのディストリビューションによって違うみたいです(後述)。
またいろいろ試してみたところ、 VitualBoxやVmwareなど仮想化ハイパーバイザー側でシステムコールを処理してCLOCK_REALTIME_COARSE対応有無にかかわらず性能が変わらないといったことも見受けられました(誰か詳細わかる人教えてください)。
今回この高速化の恩恵が確認できた AWS の EC2 環境で、どこまで Date.now()が高速化できたのか、実際に測ってみました。
ちなみに、本家V8では以前に同じ対応がとられていましたが現状 revert されちゃっています。
Issue 69933005: Revert "linux: use CLOCK_{REALTIME,MONOTONIC}_COARSE" (Closed)
Chromeでビルドがフェイルしてしまったのが理由ですが、おそらく時間解像度を取得する clock_getres(2) のシステムコールが Chromeの sandbox 機能でブロックされちゃったのが原因だろうと想像しています。Nodeは関係ないので独自にパッチを当てたのでしょう(0.11系にはまだ未対応)。
2. CLOCK_REALTIME_COARSEが使えているか確認する
先述した通り、CLOCK_REALTIME_COARSEの時間解像度が1msec以下であることが必要ですが、チェックしてみましょう。
この時間解像度の値は、Linuxのタイマー割り込み頻度の設定に依存します。kernelをビルドする時に CONFIG_HZ で指定されたものです。タイマー割り込みの頻度は、OS全体のパフォーマンスに影響するのですが、各ディストリビューションで提供されているカーネルの設定値は、歴史的な経緯もあってばらつきがあるようです。
参考: FedoraとUbuntuのLinuxカーネル設定の比較
RedHat系(Fedora,CentOS含む)は、CONFIG_HZ は 1000, Ubuntuは 250のようです。
実際に確認してみましょう。
# EC2(m3.medium)のFedora20 [fedora@ip-172-31-45-147 ~]$ grep CONFIG_HZ /boot/config-3.11.10-301.fc20.x86_64 |grep -v \# CONFIG_HZ_1000=y CONFIG_HZ=1000 # EC2(m3.medium)のUbuntu14.04 ubuntu@ip-172-31-43-202:~$ grep CONFIG_HZ /boot/config-3.13.0-24-generic |grep -v \# CONFIG_HZ_250=y CONFIG_HZ=250
次に実際にCLOCK_REALTIME_COARSEの時間解像度を求めてみます。
clock_getres() を使用したクロック精度の比較で紹介されている時刻精度を取得するサンプルコード(show_clock_getre)を少し改変して実行すると、
# EC2(m3.medium)のFedora20 [fedora@ip-172-31-45-147 ~]$ ./show_clock_getres CLOCK_REALTIME: 1ns CLOCK_REALTIME_COARSE: 1000000ns # EC2(m3.medium)のUbuntu14.04 ubuntu@ip-172-31-43-202:~$ ./show_clock_getres CLOCK_REALTIME: 1ns CLOCK_REALTIME_COARSE: 4000000ns
うわぁー、やっぱりUbuntuじゃVDSOの恩恵は受けられなさそうです。
次に、 straceを用いて Date.now() 実行時の時刻取得システムコール(clock_gettime(2))を見てみましょう。
# EC2(m3.medium)のFedora20 [fedora@ip-172-31-45-147 ~]$ strace ./node_0_10_28 -e 'Date.now()' 2>&1 |grep gettime clock_gettime(CLOCK_MONOTONIC, {873, 33640556}) = 0 # EC2(m3.medium)のUbuntu14.04 ubuntu@ip-172-31-43-202:~$ strace node-v0.10.28/node -e 'Date.now()' 2>&1 |grep gettime clock_gettime(CLOCK_MONOTONIC, {4423, 358081345}) = 0 clock_gettime(CLOCK_REALTIME, {1399488630, 840884468}) = 0 clock_gettime(CLOCK_REALTIME, {1399488630, 846318927}) = 0 clock_gettime(CLOCK_REALTIME, {1399488630, 883844301}) = 0
Fedoraの方は、CLOCK_REALTIMEの時刻取得のシステムコールがないので、VDSOが効いているのがわかります。
3. どれだけDate.now()が速くなるのか?
さぁ、どれだけ速くなるのかベンチマークです。100万回 Date.now()を実行してみます。
// date_now.js var i = 1000000; while (--i > 0) Date.now();
Node-v0.10.26とNode-v0.10.28で実行時間を比べてみます。
[fedora@ip-172-31-45-147 ~]$ time ./node_0_10_26 date_now.js real 0m1.688s user 0m0.413s sys 0m0.475s [fedora@ip-172-31-45-147 ~]$ time ./node_0_10_28 date_now.js real 0m0.114s user 0m0.039s sys 0m0.025s
おぉー! Node-v0.10.28 は、Date.now()が約15倍速くなった。
straceでシステムコール統計を取ってみるとその違いは明らかです。Node-v0.10.28では、システムコール数がわずか数百。
[fedora@ip-172-31-45-147 ~]$ strace -c ./node_0_10_26 date_now.js % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 99.96 2.625466 3 1000076 gettimeofday 0.04 0.001000 71 14 brk 0.00 0.000017 9 2 getcwd (中略) 0.00 0.000000 0 2 pipe2 ------ ----------- ----------- --------- --------- ---------------- 100.00 2.626497 1000318 15 total [fedora@ip-172-31-45-147 ~]$ strace -c ./node_0_10_28 date_now.js % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 89.55 0.000120 2 70 mmap 10.45 0.000014 2 9 close 0.00 0.000000 0 9 read (中略) 0.00 0.000000 0 2 pipe2 ------ ----------- ----------- --------- --------- ---------------- 100.00 0.000134 248 9 total
ちなみに Ubuntuで同じベンチをやってみたんですが、予想通りほとんど変わらずです。
ubuntu@ip-172-31-43-202:~$ time node-v0.10.26/node date_now.js real 0m2.430s user 0m1.104s sys 0m1.321s ubuntu@ip-172-31-43-202:~$ time node-v0.10.28/node date_now.js real 0m2.597s user 0m1.130s sys 0m1.462s
Nodeコアの中の timer で Date.now() とかを使っているので http サーバとかどれだけ速くなるだろうかと hello world のベンチをしてみたのですが、結果ほとんど変わりませんでした(他のボトルネックの方が大きい)。
先述の通り、仮想化環境やディストリビューションのカーネル設定に依存しますが、EC2上のRedHat系OSで時刻取得を頻繁に行うような処理をしているNodeがあれば、Node-v0.10.28へのバージョンアップを検討することをお勧めします。