Rustがどんな言語なのか説明する時に考えたこと

B54b44bae81038b258a307973a201c1e?s=47 Igaguri
October 05, 2019

 Rustがどんな言語なのか説明する時に考えたこと

Rust Kansai Meetup #5 発表資料
https://rust-kansai.connpass.com/event/148437/

B54b44bae81038b258a307973a201c1e?s=128

Igaguri

October 05, 2019
Tweet

Transcript

  1. Rustがどんな言語なのか説明する時に 考えたこと Igaguri Rust Kansai Meetup #5

  2. 「趣味でRust書いてます」というとありがちなこと Q. 「Rustってどんな言語なん?」 • 答え方には色々ある ◦ コードを見せる ◦ 性質や機能を列挙する ▪

    所有権とかがあって、静的型付けで、ネイティブバイナリにコンパイルできて、 …… ◦ どんな所で使われているか ▪ MozillaでFirefoxのレンダリングエンジンの Servoを記述するのに使われている ◦ どんな意図で作られたのか ▪ →本日のメイン
  3. Rustの設計目標(1/4) • https://prev.rust-lang.org/en-US/faq.html のQ&Aに設計目標が掲載されている。 (Rustプロジェクトの目標は)安全であり、並行性をサポートし、実用的なシステムプログラミング言語を設計・実装することである。 Rustが存 在している(作られた)のは、他のプログラミング言語の抽象化と効率性の(両立の)水準が満足できるものではないからである。 特に、 1. 安全性についてあまり考慮されていない

    2. 並行性に関するサポートが貧弱 3. 使い方が自然にはわかりにくい 4. リソースを制御する手段があまり提供されていない などの問題点がある。 Rustは上記の問題点を改善して効率的なコードと使いやすい抽象化を両立した選択肢として存在している(作られている)。
  4. Rustの設計目標(2/4) • 想定される使用場面 ◦ (ネットワーク越しに)攻撃者からの攻撃を受けうる、 CPU-boundなソフトウェア ▪ Webブラウザ、画像変換ツール、ゲーム ▪ インターネットに接続される組み込みソフトウェア

    • なぜ安全性が必要なのか ◦ インターネットの荒野ではソフトウェアの脆弱性はほぼ確実に犯罪者に利用されるから ▪ 乗っ取り・情報漏洩・ボットネット ◦ 特にメモリ安全性は、破られた時にメモリ内容の漏洩や任意コード実行につながる • なぜ高速性が必要なのか ◦ 早く結果を返すため ◦ リソースを節約するため ◦ 電力を節約するため
  5. Rustの設計目標(3/4) • 既存言語と比べる ◦ 高速性はあるが安全性が不足している言語 ▪ C, C++, アセンブリ ◦

    安全性はあるが高速性が不足している言語 ▪ GCを採用するほとんど全ての言語 • Java, C#, Haskell, Go, ……
  6. (補足)具体的なプログラミング言語間の速度差 https://benchmarksgame-team.pages.debian.net/benchmarksgame/which-programs-are-fastest.html CPU-boundなベンチマークの結果が集計されている(対数グラフに注意)

  7. Rustの設計目標(4/4) • 高速性 ↔ 安全性 というトレードオフ ◦ プログラマによるメモリ管理による高速性。安全性について妥協する。 ◦ 実行時にGCを使うことによるメモリ安全性。高速性について妥協する。 •

    Rustは高速性と安全性を両立する ◦ 高速性 ↔ 安全性 ↔ 学習コスト という3点間のトレードオフに変形 ▪ 高速性と安全性を取り、学習コストについて妥協する Rustがどのように高速性と安全性を両立しており、そのために新たに何を学ばなければ ならないかということが、Rustの特徴の説明になる
  8. (例) 型チェック戦略 (1/2) • C/C++ ◦ 弱い静的型付け ▪ 型システムは安全性を網羅的に保証せず、未定義動作になるパターンが数多く存在 ◦

    (C++) 仮想関数はオプション ▪ 継承による多相性を使わないなら、仮想関数テーブルを経由しないので速い • Java ◦ 強い静的型付け + ダウンキャスト等の実行時チェック ▪ 不正なダウンキャストは例外を出すので、未定義動作にはならない ◦ メソッド呼び出しはデフォルトで動的ディスパッチ ▪ 継承のサポートのために必要 ▪ 不要な場合JITで消去される(が、JIT自体に実行時コストがかかる) • Ruby ◦ 実行時に型タグを検査(いわゆる「動的型付け」) ◦ インスタンス変数もメソッドもテーブル上にある
  9. (例) 型チェック戦略 (2/2) • Rust ◦ 強い静的型付け ▪ コンパイル時に使われている値の型の整合性を保証 ▪

    実行時チェックが不要なので高速 ◦ 継承がない ▪ ないので仮想関数テーブルを用意しなくていい ▪ 動的ディスパッチが欲しければトレイトオブジェクトを使用する • Box<dyn T>などに仮想関数テーブルが含まれている
  10. (例) 所有権, 借用, ライフタイム (1/2) • 問題: Double-Free、Use-After-Free等のメモリ管理系の脆弱性 • CやC++での扱い

    ◦ 変数はスタックとヒープの両方に確保できる ▪ 関数内でしか使わないなら、スタックに置くほうが高速なので嬉しい ◦ プログラマがmalloc / freeを使って管理する ▪ ミスは良くてメモリリーク、悪いと未定義動作を引き起こす ◦ C++には限定的に解決するスマートポインタが存在 • JavaやRubyなどでの扱い ◦ クラスのインスタンスは全て参照経由で扱う ◦ 参照型の参照先になるインスタンスは全てヒープに確保 ◦ プログラムは参照を使い終わったらそのままポイ捨て ◦ GC機構が実行中に参照の有効無効を判定してメモリを回収する ▪ 様々な工夫があるが、それでも実行時コストがかかる
  11. (例) 所有権, 借用, ライフタイム (2/2) • Rustの扱い ◦ 所有権 (ownership)

    ▪ 値には単一の所有者が存在する ▪ 所有者は自身が開放される時、所有する値を開放する 管理責任を持つ ◦ 借用 (borrowing) ▪ 所有者以外は限定的な操作のみが許され、 freeなどはできない ▪ 可変参照と共有参照の区別がないと、 Use-Afer-Freeを起こせる • Vecの要素への共有参照がある状態で Vecを拡大した場合など ▪ 循環参照を作らせない効果もある ◦ ライフタイム (lifetime) ▪ 借用である参照の期間をチェックする ▪ Use-After-Freeを回避 ownership
  12. (例) 静的に解決されるジェネリクス (1/2) • C ◦ そんなものはない • C++ ◦

    コンパイル時に具体的な型のそれぞれについて展開されるテンプレート ▪ 具体的な型が埋め込まれたコードになり、最適化が効きやすく高速 ▪ 型の保証は不十分(展開後のコードが不正な時はコンパイルエラーにはなる) • Java ◦ 具体的な型(メソッド実装)が動的に解決されるジェネリクス ▪ Javaのジェネリクスは型検査後に型消去され Objectクラス扱いになる ▪ インスタンスを使う時は自動的にダウンキャストをして使用 ▪ プリミティブ型が使えないのはこの機構のせい • Ruby ◦ ダックタイピングなのでそもそもジェネリクス機構という概念が存在しない ▪ 実行時にインスタンス変数やメソッドの有無を判定している(遅い)
  13. (例) 静的に解決されるジェネリクス (2/2) • Rust ◦ コンパイル時に具体的な型のそれぞれについて展開されるジェネリクス ▪ C++と同じく、通常のコードに展開されるため最適化が効きやすい ▪

    C++と同じく、(T: Sizedなら)スタックに確保できるので速度性能が良い ◦ トレイトを使用して型の範囲を制限できる ▪ 展開前に型エラーを出せるため、エラーメッセージが人間に優しくなる ▪ T::default() のようにトレイトにあるメソッドではない関数も呼び出せる
  14. まとめ & 余談 • まとめ ◦ 設計目標の観点から Rustの特徴を説明するアプローチがある ▪ 高速性と安全性を両立し、そのために導入した機構に関する学習コストを犠牲にしている

    ◦ Rustの基本的な仕様は、設計目標を達成するように選ばれている ▪ 型、所有権・借用・ライフタイム、ジェネリクスの仕様、継承の排除 …… • Rustの仕様をきちんと理解するには、コンピュータに対する理解が必要 ◦ なぜデータをスタックに置くほうが好ましいのか ▪ ヒープメモリの確保のコスト、キャッシュメモリの動作等 ◦ なぜ動的ディスパッチがあると遅いのか ▪ 仮想関数テーブルの動作原理