#導入
今までJava、Pythonしか触ってこなかったので
今更ながらRUSTの特徴を勉強していきたいと思います。
今回は、RUSTの速さがどういう仕組みで実現できているか?の
さわり部分を調べてみたいと思います。
#RUSTとは?
###RUSTの概要
- Mozillaが支援するオープンソースのシステムプログラミング言語
- C言語、C++に代わる言語を目指している
- 手続き型プログラミング、オブジェクト指向プログラミング、関数型プログラミングなどの実装手法をサポート
- 実行時速度性能はC言語と同等程度
- 強力な型システムとリソース管理の仕組みにより、メモリ安全性が保証されている
###RUSTを構成する3つの柱
- パフォーマンス
- メモリ効率が高くランタイムやガベージコレクタがない
- 組込み機器上での実行・他の言語との調和も簡単にできる
- 信頼性
- 豊かな型システムと所有権モデル
- メモリ安全性とスレッド安全性が保証されている
- 様々な種類のバグをコンパイル時に排除可能
- 生産性
- 優れたドキュメントとコンパイラ
- 統合されたパッケージマネージャとビルドツール
- 多数のエディタに対応するスマートな自動補完と型検査機能、自動フォーマッタ
ほぼ公式からのコピペですがこんなところでしょうか。
今回は太字箇所の、速度と安全性を掘り下げて行きます。
##RUSTはなぜ速くて安全なのか?
RUSTの特徴としては以下の3点があげられます。
- ガーベジコレクションがない
- 直接機械語にコンパイルされる
- ゼロコスト抽象化
####1.ガベージコレクションがない
GCといえばJavaが有名ですよね。
GC自体は悪いものではないのですが、アプリケーション性能が低下する一番の要因ですし、
どんなに頑張っても世代別GCを利用するシステムではGCが発生してしまいます。
参考:JavaのGCの仕組みを整理する
RUSTのメモリ管理の基本的な考え方は以下で表すことができます
- 関数のスコープから外れる 又は 所有権が譲渡されたオブジェクト → 破棄される
- 値を関数へ代入する → 所有権が譲渡される
- 所有権を渡したくないとき → 参照を使う
- これらをコンパイル時にチェックできる
RUSTは上記の所有権の考え方を用いることで、オブジェクトを解放するべき場所がコンパイラ目線からも分かるため、 GCがなくてもメモリを正しく解放できます。
そのため、人間がfile.Close()
やfree()
等「リソースを解放するための操作」は Rust では不要となります。
例えるならば、人間は絶対メモリリークさせるので、オブジェクトは全部コンパイラ側で破棄前提で管理されます という、
性悪説的な考え方を持っている って感じでしょうか。
####2.直接機械語にコンパイルされる
RustはC、C++同様に、直接機械語を生成して実行しています。
Javaやpythonは、マルチプラットフォーム上で動かすために実行時にバイトコード(中間コード)を生成し、仮想マシン(PVM,JVM等)で実行します。
このため、速度では機械語で実行されるRUSTの方に分配が上がりますが、
利用するライブラリをプラットフォーム毎に調整する必要がある事は覚えておく必要があるでしょう。
参考:RustがC++に速度で勝った話
参考:Java 仮想マシン (JVM)
####3.ゼロコスト抽象化
ゼロコスト抽象化とはRUST特有の考え方で、文字通り、コストがない(ゼロ)抽象化のこと
C++の概念である、**「ゼロオーバーヘッド原則」をもとにしていて、
一言でいうと「実行することに対して余計なコード(GCや間接参照)でCPUを消費しない」**という考え方です。
プログラミングにおける抽象化とは、共通部分を抽出し、その詳細をブラックボックス化することです。
これにより仕様変更に強く、再利用性の高いプログラムを開発できます。
抽象化の例:
def transfer_gold(amount, user_id)
user_a = User.find(user_id)
user_a.gold -= amount
user_a.save!
end
上記は「user_idのユーザーの口座にamount円を振り込む」という関数です。
引数として渡されたユーザーや金額等の値の処理方法は記されていますが、
実際に関数が呼び出されるまで値はわかりません。
このように、「具体的な判断を遅らせることで柔軟性を得る事ができる」、が抽象のメリットですが、
正確な型は実行時になって初めて判明するため、インライン化(コンパイラによる最適化手法)がきないため、コストがかかるというデメリットがあります。
さて、上記のコストとは、
**メモリの動的利用 (動的ディスパッチ)**のことを指します
例えば、aomunt
で扱いたいデータのサイズが不明なときや、user_id
の型が状況に応じて異なる場合には、動的なメモリの確保と解放を利用します。
これらの、
- メモリを確保する
- メモリが使われていないことを検知する
- 使われていないメモリ領域を解放する
といった判断をプログラムがするときに、コストがかかる という事です。
一方、RUSTでは、抽象化された関数が実際に使っている具体的な型をコンパイラ当てはめて、具体的な関数を作るという、**「静的ディスパッチ」**を前提にして動いています。
呼び出し箇所はこれらの具体的な関数を呼び出す形で置換されるから、確保/解放といった余計なコストがなくなります。
例えば、
def main()
transfer_amount = 10000
user_id = "taro"
transfer_gold(transfer_amount, user_id)
end
def transfer_gold(amount, user_id)
user_a = User.find(user_id)
user_a.gold -= amount
user_a.save!
end
というソースであれば、
transfer_gold
のamount
はint型 user_id
はstring型で確保してくれます。
コレならばプログラム実行時でも利用する型が明確なのでメモリの確保も動的に行う必要がありません。
なので 抽象化に対する追加のコスト がゼロであるという理屈です。
参考:ゼロコスト抽象化とは一体何なのか
参考:rust 動的ディスパッチと静的ディスパッチの調査
もちろん、静的ディスパッチのみで実装できないメソッドも存在するため、動的ディスパッチも利用できますが、
RUSTでは静的ディスパッチをメインで使う事が前提となっているようです。
この部分はぶっちゃけ自分も理解していないところなので、
実際に手を動かして検証して見たほうが良さそうです。
#おわりに
以上、RUSTの特性ついて薄く調べてみました。
他言語との組み合わせや、MW・プロセッサ・OS等との相性なども近日中に調べていきたいと思います。