ぼちぼち日記

おそらくプロトコルネタを書いていることが多いんじゃないかと思います。

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.11.14が0.11系の最後となる予定とアナウンス。そう、次々はいよいよNode-v0.12です。

安定版の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()の高速化を図ったものです。これを利用できる条件は、今回のパッチでは

  1. Linux (kernel 2.6.32以上)であること。
  2. 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へのバージョンアップを検討することをお勧めします。

*1:Node-v0.10.27は、npmの変数名typoによる致命的なバグが見つかったので使わないようにしましょう。