2020年8月13日木曜日

インメモリデータベースの開発言語にRustを選んだ理由

https://www.forcia.com/blog/001167.html
シェアしました。


QiitaのRust Advent Calendar 2018 4日目の記事です。
技術本部の松本です。フォルシアではインメモリデータベースをRustで開発しています。
本記事では、なぜRustを選んだかをご説明します。

速度

Rustは2015年に1.0がリリースされた比較的新しいプログラミング言語であり、
「速度、安全性、並行性」をゴールとしています。
言語の選定にあたっては、動作速度が重要視されました。
Computer Language Benchmarks Game(ベンチマーク結果を公開しているサイト)
によれば、RustはJavaやGoより高速で、C++並の速度が出ると言われています。
実際に我々が検証した際も、RustはGoよりは高速で、Javaと異なりGCが無いため、
Rustの方が好ましいという結果になりました。

ガベージコレクション(GC)が無い

Rustでは所有権の概念を取り入れることで、いつメモリ上からオブジェクトが解放
されるか、管理しています。そのためGCが必要なく、GCによるプログラムの停止を
避けることが出来ます。技術検証時にJava実装を検討しましたが、GCが走るタイミングで
レスポンスに時間がかかることがあり、安定して高速なレスポンスを返す
インメモリデータベースの用途上、Rustに軍配が上がりました。

静的ディスパッチによるゼロコスト抽象化

Rustがなぜ速いのかという文脈で「ゼロコスト抽象化」というキーワードに言及される
事が多いですが、少し噛み砕いて説明したいと思います。
Rustではジェネリクス(generics)とトレイト(trait)によって多相性(Polymorphism)を
サポートしています。ジェネリック関数は同じ名前の関数に異なる型を渡すことが
できます。下記に例を示します。
  1. use std::fmt::Debug;
  2. fn main() {
  3. fn print_debug(x: T) {
  4. println! {"{:?}", x};
  5. }
  6. print_debug(1u8);
  7. print_debug("&str");
  8. #[derive(Debug)]
  9. struct Point {
  10. x: i32,
  11. y: i32,
  12. }
  13. let origin = Point { x: 0, y: 0 };
  14. print_debug(origin);
  15. }
ジェネリック関数(上記例ではprint_debug)を用いた場合、コンパイル時点で引数の
型が全て明らかになっている(u8, &str, Point)ため、それぞれの型専用の関数が作成
されます。 実行時に型から関数への割当関係を解決する動的ディスパッチを行う場合は
実行時にその分のコストが掛かりますが、Rustはコンパイル時に解決してしまう静的
ディスパッチを使用するため、実行時にはゼロオーバーヘッドで関数を対応する呼び出す
事ができます。

開発効率

また、開発効率の面も考慮事項でした。Rustは新しい言語ではあるものの、他の言語
以上に効率良く開発できる環境が整っています。
標準的なツールセットがあることや、テストが標準ライブラリに含まれていることは
C++よりも好ましい部分です。

rustupによる簡単な環境構築

Rustは環境構築が驚くほどに簡単です。rustup.rsにアクセスし、表示されたコマンドを
実行するだけでRustを使い始めることが出来ます。
RustのパッケージマネージャであるCargoですが、パッケージ管理だけでなく、
プロジェクトの作成からテスト、ビルド時の設定まで一貫して管理してしてくれるため、
いろいろなツールを使い分ける必要がありません。もちろんrustupでインストール
されます。

コーディング時のサポート

  • Rust Language Server (RLS)が開発されており、コーディング面で補完などの
    サポートが受けられます
  • rustfmtで統一的なコード整形が可能です
上記のツール群も日々アップデートされており、不便なくツール群の恩恵に預かって
います。

エラーメッセージが親切

Rustのコンパイラの特徴として、非常に親切なメッセージが挙げられるます。
所有権やライフタイムといった馴染みの薄い概念があるため、完璧なコードを一撃で
書くことはまだ難しいのですが、基本的に「どこを」「どう」修正すれば適切な
アノテーションができるかを、コンパイラがエラーメッセージとして表示してくれます。
コンパイラのサポートを受けながらコードを書いていくことが出来ます。

言語標準のテスト

Rustは言語の標準機能としてテストが用意されており、cargo testのコマンドで実行
することが出来ます。
テスト用のライブラリ群をインストールして......といった作業が無いのは嬉しいことです。
cargo new --libした際に作られるスケルトンには、テストケースのスケルトンが
含まれています。すぐにテストを書かないとという気分にさせてくれますし、
実装とテストが同じファイルに書けるため、テストが仕様の役割を果たしやすく
なります。docコメント中のテストも実行されるので、実装とコメントが乖離する
ことも防ぎやすくなっています。 
また、テストはファイルごとに並列で処理されるため、プロジェクト全体のテストを
行う際も高速にテストが可能です。

学習コスト

Rustのデメリットとして、初期の学習コストの高さが挙げられます。
実際、借用(・所有権)・ライフタイムの概念は馴染みが薄く、ドキュメントを読み
コンパイラと格闘しながら学習していくことになりました。
確かに馴染みの薄い概念ではなるものの、ユーザーとして使う分には難しい概念では
ないため、職業プログラマが仕事として向き合ってしまえばコンパイラのサポート化で
問題なく利用できるレベルだと考えています。 文法に関して「変数の型は
アノテーション風に書く※」「ブロックの末尾の式が返り値になる」と念じれば、
他の手続き型言語と大きな違いはありません。先日の勉強会で「Rustは関数型風の文法が
書ける手続き型言語」とおっしゃっていた方がいらっしゃいました。実際手続き型言語に
馴染みのある人間からすると、パターンマッチなど関数型言語に特徴的な機能を
使うことができる手続き型言語という風に見え、文法部分の学習コストは大きく
ありません。
フォルシアでは概念と基本文法を理解するためのハンズオンを社内で行い、
Rustが書けるエンジニアを増やそうと試みています。
※JavaScriptのFlowやPythonでアノテーションを書いたことがある人であれば、いつもの位置に書くだけです。

さいごに

以上の理由から、フォルシアでインメモリデータベースを作る際に、速度と開発効率の
面でRustは良い選択肢になりました。
2019年1月16日に予定しているShinjuku.rs #2ではデモをお見せできそうです。
この記事を書いた人

松本健太郎

フォルシア3年目のエンジニア。
インメモリデータベースの開発を行っている。

0 コメント:

コメントを投稿