RubyでTCP_FASTOPENを使う

サーバ側

setsockoptにTCP_FASTOPENを渡せば良い。
TCP_FASTOPENは、trunkのr42865以降Socket::TCP_FASTOPENとして定義されている。

require "socket"

TCP_FASTOPEN = Socket::TCP_FASTOPEN || 23

serv = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
serv.setsockopt(Socket::SOL_TCP, TCP_FASTOPEN, 5)
addrinfo = Addrinfo.new(Socket.sockaddr_in(8888, "localhost"))
serv.bind(addrinfo)
serv.listen(1)

loop do
  socket, addrinfo = serv.accept
  p socket, addrinfo
  p socket.gets
end

クライアント側

RubyのSocketライブラリでは明示的にsendto(2)を呼ぶ事はできないが、
Socket#send(BasicSocket#send)の第3引数にソケットアドレス構造体をpackした文字列を渡す事で内部ではsendto(2)が使われる。
(詳しくはext/socket/init.cのrsock_bsock_send()を参照)
Server.tcpなどでTCPのソケットを作成した場合は既にconnectされてしまっているのでErrno::EISCONNが返ってくる。
そのためSocket.newから行う必要がある。
今のところtrunkでもMSG_FASTOPENは定数として定義されていない。

require "socket"

MSG_FASTOPEN = 0x20000000

socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
socket.send("foo\r\n", MSG_FASTOPEN, Socket.sockaddr_in(8888, "localhost"))

SystemTapでrubyのUser-Space Evnetsをフックする

SystemTaprubyのUser-Space Evnetsをフックしてみる。

環境は以下の通り。

systemtapのインストール

sudo apt-get install systemtap systemtap-runtime systemtap-sdt-dev

ruby 2.0.0p195のビルド

既にビルドしている場合はmake cleanしてからビルドし直す。

./configure
make
sudo make install

利用可能なUser-Space Eventsの確認

stap -L 'process("rubyのパス").provider("ruby").mark("*")'

出力例:

% stap -L 'process("/usr/local/bin/ruby").provider("ruby").mark("*")'
process("/usr/local/bin/ruby").provider("ruby").mark("array__create") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").provider("ruby").mark("cmethod__entry") $arg1:long $arg2:long $arg3:long $arg4:long
process("/usr/local/bin/ruby").provider("ruby").mark("cmethod__return") $arg1:long $arg2:long $arg3:long $arg4:long
process("/usr/local/bin/ruby").provider("ruby").mark("find__require__entry") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").provider("ruby").mark("find__require__return") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").provider("ruby").mark("gc__mark__begin")
process("/usr/local/bin/ruby").provider("ruby").mark("gc__mark__end")
process("/usr/local/bin/ruby").provider("ruby").mark("gc__sweep__begin")
process("/usr/local/bin/ruby").provider("ruby").mark("gc__sweep__end")
process("/usr/local/bin/ruby").provider("ruby").mark("hash__create") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").provider("ruby").mark("load__entry") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").provider("ruby").mark("load__return") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").provider("ruby").mark("method__entry") $arg1:long $arg2:long $arg3:long $arg4:long
process("/usr/local/bin/ruby").provider("ruby").mark("method__return") $arg1:long $arg2:long $arg3:long $arg4:long
process("/usr/local/bin/ruby").provider("ruby").mark("object__create") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").provider("ruby").mark("parse__begin") $arg1:long $arg2:long
process("/usr/local/bin/ruby").provider("ruby").mark("parse__end") $arg1:long $arg2:long
process("/usr/local/bin/ruby").provider("ruby").mark("raise") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").provider("ruby").mark("require__entry") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").provider("ruby").mark("require__return") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").provider("ruby").mark("string__create") $arg1:long $arg2:long $arg3:long

SystemTapスクリプトの作成

今回は試しに例外の発生をフックしてみる。
まず以下のようなSystemTapスクリプトを書く。

raise.stp:

probe process("/usr/local/bin/ruby").provider("ruby").mark("raise") {
  printf("%s %s %d\n", user_string($arg1), user_string($arg2), $arg3)
}

以下のコマンドで実行:

sudo stap <SystemTapスクリプト> -c <実行するコマンド>

出力例:

% sudo stap raise.stp -c "ruby --disable-gems -e 'raise rescue nil'"
RuntimeError -e 1

RuntimeErrorの発生をフックする事ができた。

IO.copy_streamで出力先がsocketでない場合でもsendfile()を使う

※結論から言うと速くならなかった。

rubyのIO.copy_streamは可能であればsendfile()を用いるが、これは出力先がsocketである場合に限定されていた。
Linux 2.6.33以降ではsendfile()の出力先が通常のファイルでも許されるようになっていた( sendfile(2) - Linux man page )ので、試してみた。
(実行した環境は、Ubuntu 11.10 3.0.0-15-server)

diff --git a/io.c b/io.c
index f404b53..181897f 100644
--- a/io.c
+++ b/io.c
@@ -9424,8 +9424,6 @@ nogvl_copy_stream_sendfile(struct copy_stream_struct *stp)
         stp->error_no = errno;
         return -1;
     }
-    if ((dst_stat.st_mode & S_IFMT) != S_IFSOCK)
-        return 0;
 
     src_offset = stp->src_offset;
     use_pread = src_offset != (off_t)-1;

このpatchをtrunkに適用すると通常のファイル同士のコピーでもsendfile()を使うようになる。
(実際にsendfile()を使っているかどうか確認するにはstraceが便利)

dd if=/dev/zero of=dummy bs=1M count=100

として作成したファイルを使って

require 'benchmark'

Benchmark.bm do |x|
  x.report do
    IO.copy_stream("dummy", "dst")
  end
end

このようなベンチマークを実行した。
それぞれ実行前に

sudo sysctl -w vm.drop_caches=1

した上での結果である。

trunk(r34734) # read() and write()

       user     system      total        real
   0.000000   0.360000   0.360000 (  0.702267)

       user     system      total        real
   0.000000   0.370000   0.370000 (  0.755613)

       user     system      total        real
   0.000000   0.390000   0.390000 (  0.724757)

proposal # sendfile()

       user     system      total        real
   0.000000   0.330000   0.330000 (  0.713097)

       user     system      total        real
   0.000000   0.400000   0.400000 (  0.737590)

       user     system      total        real
   0.000000   0.350000   0.350000 (  0.717704)

速くならなかった。
man pageには

Because this copying is done within the kernel, sendfile() is more efficient than the combination of read(2) and write(2), which would require transferring data to and from user space.

とあるけれど、通常のファイル同士のコピーではそれほど恩恵が得られないようだ。

セキュリティ&プログラミングキャンプ2011に参加した

プログラミング言語クラスに参加した。
講義の内容についてはどこかで見られると思うので、個別課題について書く。

個別課題の内容

rubyのpreludeのprecompileという課題に取り組んだ。
preludeというのはインタプリタの起動時に実行されるRubyスクリプトで、内容は

である。

preludeのRubyコードはprelude.cにCの文字列として埋めこまれていて、rubyは起動時に毎回このコードをYARVの命令列にコンパイルして実行している。
preludeの内容は一旦ビルドしてしまえば変わらないので、前もって命令列にコンパイルしておいて(precompile)、インタプリタの起動時にはこれを直接実行すれば高速化が期待できるのではないかと考えた。

結果

prelude.cはrubyのビルドプロセス内でtool/compile_prelude.rbから生成されているので、ここを弄った。
まずはpreludeの内容をcompileしてファイルに書き出しておき、rubyの起動時にそれを読んで実行するという手っ取り早い方法を試したが、これではprecompile無しと比較して6%ほど遅い。

そこで命令列をprelude.cに埋め込んでみたが、これでも3%ほど遅くなってしまった。

考察

prelude.cに命令列を埋め込んでもまだ遅いという事は、コードをcompileするよりも文字列から命令列を得る方がかえって時間がかかるという事を示している。
つまりMarshal.loadでは遅い。
precompileで高速化を狙うには、命令列を高速にloadできるような形式を考える必要がある。

発表

やっている事はシンプルなのに、発表後に「難しい事をされていたんですね」と参加者から言われてしまった。
発表時間が2分しか与えられなかった事を差し引いても、限られた時間で要点を伝える能力の不足を感じた。