Rustでrbenvよりもinitが6倍速いRubyバージョンマネージャーを作った
こんにちは、TaKO8Kiです。
この記事では、本日、2021年9月17日にRust.Tokyo 2021で「Rustでrbenvよりも7秒速いRubyのバージョンマネージャーを作った」というタイトルで発表した内容についてまとめています。
スライドは、こちらにあります。
作ったもの
frumというRust製Rubyバージョンマネージャーを作りました。
きっかけは、具体的に何か課題があったというよりは、rbenv install
を10秒くらい高速化できたら面白そうだなという軽い気持ちです。
rbenvを使ったことある方は分かると思いますが、普通にrbenvと同じような感じで使えます。
$ eval "$(frum init)"
$ frum install 2.6.5
$ frum local 2.6.5
ベンチマーク
タイトルには「rbenvよりもinitが6倍速い」とありますが、ベンチマークを見てみましょう。
まず、eval "$(rbenv init)"
→ rbenv install
→ rbenv local
という一連の流れのベンチマーク結果は、下記のようになりました。これを見てみると、frumの方がrbenvよりも1.04倍速い(約7秒)ことが分かります。当初の目標ほどは速くならないという結果になりました。
eval "$(frum init)"
→ frum install
→ frum local
vs
eval "$(rbenv init)"
→ rbenv install
→ rbenv local
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
rbenv |
239628.1 ± 2030.2 | 237681.6 | 245162.6 | 1.04 ± 0.01 |
frum |
232944.6 ± 1224.0 | 230565.4 | 234863.5 | 1.01 ± 0.01 |
frum (pre-release) |
230366.5 ± 882.7 | 228454.2 | 232340.5 | 1.00 |
次に、rbenv init
とfrum init
を比較したベンチマークは下記のようになりました。これを見ると、frum init
がrbenv init
よりも6.14倍速くなっていることが分かります。
frum init
vs
rbenv init
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
eval "$(rbenv init -)" |
49.5 ± 2.1 | 46.2 | 57.2 | 6.14 ± 0.50 |
eval "$(frum init)" |
8.1 ± 0.7 | 7.0 | 11.8 | 1.00 ± 0.11 |
eval "$(frum init)" (pre-release) |
8.1 ± 0.6 | 7.2 | 11.7 | 1.00 |
このように、installコマンドは速くはなったが、体感としてはさほど変わらない。一方で、initコマンドは6倍速くなったので、rbenvよりもシェルの起動を高速化することができました。
ベンチマークはGitHub Actionsを用いて取りました。詳細はこちらをご覧ください。
どこをどう改善したのか
bash scriptで書かれているruby-buildやrbenvをRustで書き直したのはもちろんですが、主にrbenv init
・rbenv rehash
で使用されているshimsというものを使用するのをやめ、別の方法にしました。
shimとは
Wikipediaでは、
コンピュータ・プログラミングにおいて、シムとは、APIコールを透過的に傍受し、渡された引数を変更したり、操作自体を処理したり、操作を別の場所にリダイレクトしたりするライブラリのことである。
と書かれています。
shimは直訳すると(すき間などに入れる)詰め木という意味らしいです。
rbenvではshimsがどのように使われているのか
rbenvを使用している状態で~/.rbenv/shims
配下を見ると下記のようになっています。
これがrbenvでいうshimsで、その中にはbundle、gem、rubyなど色々あります。
# ~/.rbenv/shims
$ ls
bundle bundler erb gem irb rake rdoc ri ruby
それらの中身は全て同じ内容で、下記のようになっています。
#!/usr/bin/env bash
set -e
[ -n "$RBENV_DEBUG" ] && set -x
program="${0##*/}"
if [ "$program" = "ruby" ]; then
for arg; do
case "$arg" in
-e* | -- ) break ;;
*/* )
if [ -f "$arg" ]; then
export RBENV_DIR="${arg%/*}"
break
fi
;;
esac
done
fi
export RBENV_ROOT="/Users/foo/.rbenv"
exec "/usr/local/Cellar/rbenv/1.1.2/libexec/rbenv" exec "$program" "$@"
また、実際にwhich ruby
で実行ファイルの場所を特定すると、shims配下のrubyになっていることが分かります。
$ which ruby
/Users/foo/.rbenv/shims/ruby
どのタイミングでshimsが作られるのか
実際にshimsを作るのはrbenv rehash
というコマンドです。これをrbenv init
が内部的に実行しています。じゃあ、rehashがどんなことをしているかというと、下記のような感じです。
-
~/.rbenv/shims
の存在確認を行い、無ければ作る -
~/.rbenv/shims/.rbenv-shim
の存在確認を行う(このファイルの有無で現在別に実行中のrbenv rehash
があるかどうかを判断していて、あればここでexitする) - 2.で
.rbenv-shim
が無ければ作る - インストールされている全てのRubyバージョンの/bin配下にあるファイルを全て見た後、プロトタイプファイル、
.rbenv-shim
の中身をそれぞれのshimsとしてコピーする -
.rbenv-shim
を削除する
rbenvはこれらの手順をrbenv init
を実行する度に呼んでいます。(先ほど、"shimの中身は全て同じ内容"と言いましたが、これは4で.rbenv-shim
を元にshimを作っていることから分かると思います。)
そのため、これを改善することがrbenv init
を高速化することに繋がるということです。
frumではどのような実装になっているか
frumでは、シンボリックリンクを用いてバージョンの切り替えを行なってます。該当コードは下記です。
fn create_symlink(config: &crate::config::FrumConfig) -> std::path::PathBuf {
let system_temp_dir = std::env::temp_dir();
// シンボリックリンクとして使用するのpathを作る
let mut temp_dir = generate_symlink_path(&system_temp_dir);
while temp_dir.exists() {
temp_dir = generate_symlink_pah(&system_temp_dir);
}
create_symlink_dir(config.default_version_dir(), &temp_dir).expect("Can't create symlink!");
temp_dir
}
具体的には、$TMPDIR配下にfrum_xxxxx_xxxxxxxxxxxxxx
という名前で~/.frum/versions/[バージョン]
へのシンボリックリンクを作成し、バージョン切り替えのタイミングでシンボリックリンクの向き先を変えるといったことをやっています。
このように、rbenvと違いファイルの作成や削除を行う必要がなく、単にシンボリックリンクを作成するだけなので、先ほどのベンチーマーク結果のようにfrum init
がrbenv init
よりも約6倍速くなっています。
最後に
結果、frum init
がrbenv init
よりも約6倍速くなり、シェルの起動時間を短縮することができました。実際に、バイト先のRubyのバージョン管理にはfrumを使っています。
また、最近は、RustでgobangというTUIのSQLクライアントを作ったりしてるので、そちらも良かったら見てみてください。
以上です。ありがとうございました!
Discussion