FORCIA CUBEフォルシアの情報を多面的に発信するブログ

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

2018.12.04

アドベントカレンダー2018 Rust テクノロジー

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)をサポートしています。ジェネリック関数は同じ名前の関数に異なる型を渡すことができます。下記に例を示します。

use std::fmt::Debug;
fn main() {
    fn print_debug(x: T) {
        println! {"{:?}", x};
    }
    print_debug(1u8);
    print_debug("&str");

    #[derive(Debug)]
    struct Point {
        x: i32,
        y: i32,
    }
    let origin = Point { x: 0, y: 0 };
    print_debug(origin);
}

Rust Playgroundで動かしてみる

 ジェネリック関数(上記例では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年目のエンジニア。
インメモリデータベースの開発を行っている。