Upgrade to Pro — share decks privately, control downloads, hide ads and more …

コンパイル時計算への招待.pdf

take_cheeze
November 25, 2017
1.2k

 コンパイル時計算への招待.pdf

take_cheeze

November 25, 2017
Tweet

Transcript

  1. 自己紹介 • mruby四天王最弱くらい(そんなに強くないよ • 周りが強くなるともっと弱くなるはず • 今年の6月くらいに福岡に引っ越してきた • 好きな言語はC++/Rust •

    Rubyはメタプロが好き(にっこり)、そして自分の足を撃つ! • 最近、仕事でツールほしい時にGolangを書くことが • 気がついたら、今日の発表でハンドルネームなの自分だけ
  2. 話すこと • コンパイル時計算! • のための準備(?) • 主にC++ • コンパイルして実行ファイルを吐く言語が長いので •

    C++17は、それほど把握していないので話せません • CppConの動画はYouTubeで見れるので… • Conceptはまた延期みたいです • あとは、ブレまくるおまけ • あんまりコードはない
  3. C++のコンパイル時計算とは • “Compile time function execution”(CTFE)とか • 場合によっては”execution”を”evaluation”と言う場合も • 実行時の代わりにコンパイルする時に関数を実行する

    • 実行時コストをコンパイル時に払える • templateや定数式を用いる • 規格上、定数式を実行時に回してもおkであったり • Lispのマクロに相当するもの?(LISP詳しくないので… • 他の言語でも「うっかりチューリング完全」からできたり • 最近の言語だと、最初から意図的に入れている機能
  4. コンパイル時計算が発達した背景 • コンパイラ最適化に頼った高速化の限界 • 高速化のために最適化を行うほど意図しないコードが生成される • コンパイラの内部で行っている最適化の組み合わせを把握するのが難しい • 動的ディスパッチはコストが大きい •

    De-virtualizationという最適化 • コンパイル時に決定できることを実行時に行う必要性のなさ • 黒歴史と化しつつあるiostream • 特別なツールの導入無しで行える • 諸々の事情で、ツール導入が難しいのはよくある • ビルドシステムで特別なタスク追加は意外と難しい?
  5. JITコンパイルによる高速化との違い • コンパイル時計算はAOTに分類(?)されるので大体JIT vs AOT • 実行時プロファイリングが必要ない • 実行時メモリコストが減る •

    動的コンパイルに必要なメモリが不要 • メモリ最適化が実行時コストを払わず行える • 最適化戦略がプログラマ任せ • JITは実行時プロファイリングを元に自動的に最適化してくれる • AOTでもProfile-guided optimizationという似た手法がある • プログラムへの理解度が低いほど最適化がしづらい • De-optimizationという謎テクニックが不要(当然対価は払うが)
  6. ざっくりとした歴史 • 詳しくは是非C++標準化委員会の方にきいてください! • C++のtemplate機能の導入時に整数値を渡せることから発見される • Boost C++ Librariesなどで、テンプレートメタプログラミングとして発 達する

    • C++11で、constexprという機能が導入される • templateを使わずともコンパイル時計算が可能に • C++14で、constexprの制限が緩和、使いやすくなった • より、C++らしい文法でコンパイル時計算が記述できるように • 現状、C++でおすすめの規格はC++14
  7. template時代 • テンプレートメタプログラミング未満くらい(?) • 型から、効率的なデータ構造を作成するのによく用いられた • 顕著なのが: boost::array<int, 10> •

    C++11以前の話なので、stdではない • 言語によっては、可変長配列などでやってしまう • ::size()というメンバ関数があるが、単に定数を返す(この場合10) • 配列の大きさを構造体メンバに含める必要がない • 実行時のチェックが減る • ヘッダーのみライブラリによる移植性
  8. C++11以前のTMP時代 • Boost C++ Librariesで盛んな印象 • templateの多用などで可読性が悪い • いわゆる、「型ベース」といわれるものが主体 •

    整数演算がコンパイル時ですでにできたので、enumを駆使したり • ヘッダーのみライブラリの一般化 • templateを多用するため動的ライブラリとして型を決定できなくなった • ヘッダーファイル読み込みのI/Oでさらなるコンパイル時間の増加 • 具体的な型を書かずに済むような方向へ発展
  9. C++11時代 • 長きC++の停滞を打破 • constexprが言語機能として実装された • コンパイル時レイトレーシングをする人がいた • 実質return文だけの関数しか定義できなかったので不便だった •

    関数型プログラミングっぽさを強制はされた • constexprをコンパイル時に実行したときに例外を投げるとコンパイル 時エラーに • 詳しくは”Sprout”、中3女子、ボレロさんあたりでぐぐってください
  10. C++14時代 • 定数式関数外に副作用が漏れなければ、関数内で副作用を許容 • 定数式内にとどまるなら、変数への再代入ができる • 複数の文を関数内で書くことができるように • Boost.Hana による「値ベース」のメタプログラミング

    • C++03の実行時表現みたいなメタプログラミングが可能に • 定数式プログラミングが一定の完成をみた • ECMAScript 2015と同じで、黒魔術的なテクニックやツールを使わず とも標準機能で満足な(メタ)プログラミングが • 具体的な型を書く機会が減ってとてもうちやすい
  11. サンプル:CTFEで階乗を計算するだけ • 長いので: https://wandbox.org/permlink/gTW0kZuDc1ffB3mh • 上からC++98, C++11, C++14っぽい書き方で • 古い書き方でもちゃんとコンパイルできるのがC++のよいところ

    • C++17っぽい書き方は…知らぬ • Wandboxとは? • 無料で使えるオンラインコンパイラ(スポンサー募集中みたいです) • いろんなバージョンが揃っているのでバージョン依存の検証などに • C++に強い方が作ってるのでC++コンパイラがとても充実している • スクリプト言語も揃っている(Rubyもmrubyもある)
  12. うれしくないところ • 実行時計算からは外れた書き方をする(C++14で改善) • エラーに慣れるまでわかりづらい • 実行時計算とのトレードオフで、コンパイル時間が長くなる • 結果、開発サイクルが遅くなる •

    コンパイル時計算に対応したライブラリがあるとは限らない • 規格や流行の時期によって、書き方がかなり変わる • constexprの実現にはC++インタプリタが必要
  13. Rubyのメタプログラミング • すべてが実行時に行われる • C++にあるコンパイル時制約がないので、自由に行える • メタプログラミングをRubyの表現から離れずに行える • もはや、Rubyの表現となっているためC++ほど違和感がないとも •

    C++だとコンパイル時に行えることが実行時にしか行えない • やりすぎるほど効率が落ちてく印象 • メソッド定義をevalなどで上書きするなどの方法はあるが可動性は低下 • 便利さと効率がトレードオフなのは嬉しくない
  14. 現在のRubyのコンパイル方式 • 詳しくは Compiling Ruby を • 大体、以下の4つの形態をとって最後はVMに投入される • スクリプト(生のソースコード)

    • トークン列 • 抽象構文木 • YARVコード(通称ISeq(Instruction Sequence)など) • YARVコードですらまだかなり実行時処理を残している • コンパイルする言語で静的に行うクラス・メソッド・定数の定義など • コードカバレッジでメソッド定義すらカバーされる
  15. メソッドが動的解決される • メソッドの再定義がどこまでも許容されている • クラス定義時は型がかなり流動的である • 型チェック導入の高いハードル • 最終的に呼ばれるCの拡張メソッドの型はかなり静的 •

    Object型として扱うとしても要求する振る舞いがある • すべてのクラス・メソッド・定数がVM上に読み込まれて、main関数の 実行を残すのみになると再定義したいケースが少ない • pryなどで利用したいケースもあるが、リリース後はほぼしない…はず • Rubyのオーバーヘッドでよくやり玉に
  16. 不変な実行環境 • クラス、メソッド、定数の変更を特定の地点から禁止する • 特定の地点からメソッドを静的解決できる • プログラムを定義部と実行部を分離する • すべてのライブラリを読み込んだ状態のVMをmain関数から最適化していく •

    必要なgemをすべて読みこんだら実行環境を変えることはそうない • メソッドスコープ内だけが実行部(?) • Truffle Ruby と相性良さそう • 不変オブジェクト化することで型チェックもより容易に導入 • 開発時の利用は特に想定しない
  17. 基本方針 • 全モジュール読み込みなどの定義部をコンパイル時計算とみなす • 全モジュールの読み込みが終了した後以下の変更を禁止する • メソッド • クラス •

    定数 • 固定化するオブジェクトの参照透過性 • 今までのRubyではできなかった最適化を行う • メソッドが静的に決まれば部分実行もできる • main関数などのエントリポイントの追加
  18. 実装方法(?) • ASTとYARVの間くらいで、全モジュール読み込みをはさむ • いくつか考えられる方式: • Ruby1.8時代のようにAST実行を行ったクラス情報を元にコード生成 • VM上で定義部を実行してmain関数から部分実行して行くメタプログラミング •

    上の2つのハイブリッド方式 • 実行時とコンパイル時の分離 • Marshal.dump/loadできないオブジェクトは実行時オブジェクト? • 細かいところでとても紛糾しそう… • 場合によっては定数からクラスメソッドになるものも?
  19. 部分実行 • 定数式扱いできそうな部分をコンパイル時計算したい • いくつか制約を設ける • メソッドの入力が定数 • メソッドが変更可能なデータ(frozenでない)を参照しない •

    そもそもランタイム実行とマークされていない • 基本的に `def` スコープの外だとほぼ実行可能? • classやmoduleスコープはほぼコンパイル時評価にできる • TypeErrorやArgumentErrorやNoMethodErrorのコンパイル時判定
  20. ともあれ検証:Rails • とりあえず、Railsで性能が出るとウケるらしいので • やったこと • Rubyにメソッドの追加定義を禁止する改変 • 雑な定義したRailsをeager_loadで実行 •

    WEBサーバーが立ち上がる直前くらいでメソッドの追加定義禁止 • Pumaのsingle.rbをいじる感じで • 困ったりするのか実証
  21. 結果:Rails • とりあえず「大規模なコード改修を入れないと無理」とだけ • eager_load しても読み込みが遅延されるクラスは残る • autoload はとても高いハードル •

    requireで上書きしてもいいが… • そもそも、Railsのモジュール設計がautoload前提なので、require追加が大変 • 嬉しいことに廃止予定らしい: https://bugs.ruby-lang.org/issues/5653 • メソッドの遅延定義による最適化と相性が悪すぎる • 普通のwebページを出すところまでいけず…
  22. もう一個検証: Hanami • 方法は大体一緒 • なんか流行っているらしいので • autoloadに依存しない構造なのでRailsほどひどくはなかった • http_router

    でこけまくる • route をevalでコンパイルして最適化するため • こちらも普通のHTTPの200なページを出すところまでいけず • Rack難しかった…
  23. 他あったら嬉しい方向性とか • Ruby as a VM optimizer • 最適化されたVMを生成するための言語としてのRuby •

    クラス・メソッド・定数定義ですら評価(eval)してる言語 • Emacs Lispにできて我々にできないわけがないのでは? • キーワード引数の最適化 • キーワードの線形探索やハッシュ探索が不要になる • メソッド静的解決できると普通のメソッドと同等のコストで呼べるように • 人類に2引数以上のメソッドは早すぎる
  24. とりあえず結論とか • JIT以外のナウくない高速化方法はある • コンパイル時計算!!!(これだけ覚えてってください!) • メソッドの静的解決(どちらかというと前提条件) • Rails難しい •

    できることなら一つの言語で両方解決したい • 開発中は動的な方がやっぱりいい • 運用からはうまいこと静的な方向に行きたい(テストでつらい) • golang とてもいいけど、もっと言語内でメタプロしたい • C++の構文(反面教師として以外)以外は見習うところが多い
  25. おまけ:Rust • 今回は、あえて触れず • traitとかがキレイなので、Rubyに導入するのが難しそう • Rustはより強い制約を持つので静的ダックタイピングが難しそう • 個人的にC++を学んだ後にやる言語だと思ってる •

    別の関数型言語のバックグラウンドがあればやってもいいとは思う • 言う人に言わせると未来のC++らしい • C++も当然発展していて新しい規格でかなり使いやすいので
  26. おまけ:Swift • Appleの現在の主力言語 • 中身はObjective-Cのランタイムベース • Objective-Cはメッセージパッシングなオブジェクト指向なのでとても動的 • それでも静的型付け •

    メモリ管理は、参照カウント • ただ、コンパイラが不要なカウント操作を削っていたり • mrubyの参照カウント版は実装したことが… • 動的なランタイムの上でも静的なチェックは有効である • JVMとかJavascriptとか
  27. おまけ:Golang • RustとSwiftに触れてしまったので • バランス全振りな言語 • 妥当すぎる設計するのでつまらないときはあるが見習いたい • コード生成とかも自前でやってたりしてなかなかすごい •

    パーサーとかもGolangで実装してある • reflectつよい • ほしい機能がだいたい標準で実装済み • システムプログラミングを謳うだけあって依存の少ないバイナリ • Rubyも夢のself-hostingできるようになってほしい…!
  28. おまけ:mruby • 実は、今日話したことが部分的に既にできる • 依存モジュールだけを読み込むVMは作れる • 凝った最適化まではしていないし、不変でもない • 依存するrubyスクリプトはmrubyバイトコードにコンパイルはされる •

    イマイチ影の薄い機能… • 引数をC側で受け取る時に緩めな静的型付けを行える • 本当は、静的シンボルテーブルとか静的クラス階層にしたい… • スクリプト言語の仮想マシンバイトコードのpeep-hole最適化つらたん • Golangに負けたくはない(謎の対抗心
  29. おまけ:Erlang • 実行環境の話なので、Elixirに置き換えても可 • マクロはあるらしい • 関数型言語は、自分の足を撃つ確率が低そうで羨ましい • メモリ戦略がかなりシビアでよく考えられている •

    耐障害性とかプロセス(という名の軽量スレッド)つよい • 参照透過性のおかげで実装の中で共有して使っても問題ない部分 が多いのが羨ましい