Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
コンパイル時計算への招待.pdf
Search
take_cheeze
November 25, 2017
1
1.2k
コンパイル時計算への招待.pdf
take_cheeze
November 25, 2017
Tweet
Share
More Decks by take_cheeze
See All by take_cheeze
goluaをさわってみる
takecheeze
0
290
html5everをスクリプト言語から呼ぶ
takecheeze
0
200
mgemのCIを支える諸々
takecheeze
2
520
Go_2のドラフトを読む__エラー編_.pdf
takecheeze
0
1.4k
fukuoka.rb 祝 #100!
takecheeze
0
560
dep ensure浅掘り
takecheeze
0
370
LuaJIT as a Ruby backend
takecheeze
1
3.2k
Fukuoka Ruby Award 10th
takecheeze
1
220
mrubyにとるRubyのシングルバイナリ運用
takecheeze
0
1.1k
Featured
See All Featured
Clear Off the Table
cherdarchuk
91
320k
Side Projects
sachag
452
42k
How to Ace a Technical Interview
jacobian
275
23k
Building Adaptive Systems
keathley
37
2.1k
How to Create Impact in a Changing Tech Landscape [PerfNow 2023]
tammyeverts
44
2k
YesSQL, Process and Tooling at Scale
rocio
167
14k
A Tale of Four Properties
chriscoyier
155
22k
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
327
21k
The Straight Up "How To Draw Better" Workshop
denniskardys
231
130k
Reflections from 52 weeks, 52 projects
jeffersonlam
346
20k
5 minutes of I Can Smell Your CMS
philhawksworth
202
19k
Designing for humans not robots
tammielis
249
25k
Transcript
コンパイル時計算への招待 いかにコンパイラ最適化を越えた最適化を行うのか? @take-cheeze (株式会社Fusic 渡辺 丈)
自己紹介 • mruby四天王最弱くらい(そんなに強くないよ • 周りが強くなるともっと弱くなるはず • 今年の6月くらいに福岡に引っ越してきた • 好きな言語はC++/Rust •
Rubyはメタプロが好き(にっこり)、そして自分の足を撃つ! • 最近、仕事でツールほしい時にGolangを書くことが • 気がついたら、今日の発表でハンドルネームなの自分だけ
話すこと • コンパイル時計算! • のための準備(?) • 主にC++ • コンパイルして実行ファイルを吐く言語が長いので •
C++17は、それほど把握していないので話せません • CppConの動画はYouTubeで見れるので… • Conceptはまた延期みたいです • あとは、ブレまくるおまけ • あんまりコードはない
C++のコンパイル時計算とは • “Compile time function execution”(CTFE)とか • 場合によっては”execution”を”evaluation”と言う場合も • 実行時の代わりにコンパイルする時に関数を実行する
• 実行時コストをコンパイル時に払える • templateや定数式を用いる • 規格上、定数式を実行時に回してもおkであったり • Lispのマクロに相当するもの?(LISP詳しくないので… • 他の言語でも「うっかりチューリング完全」からできたり • 最近の言語だと、最初から意図的に入れている機能
コンパイル時計算が発達した背景 • コンパイラ最適化に頼った高速化の限界 • 高速化のために最適化を行うほど意図しないコードが生成される • コンパイラの内部で行っている最適化の組み合わせを把握するのが難しい • 動的ディスパッチはコストが大きい •
De-virtualizationという最適化 • コンパイル時に決定できることを実行時に行う必要性のなさ • 黒歴史と化しつつあるiostream • 特別なツールの導入無しで行える • 諸々の事情で、ツール導入が難しいのはよくある • ビルドシステムで特別なタスク追加は意外と難しい?
JITコンパイルによる高速化との違い • コンパイル時計算はAOTに分類(?)されるので大体JIT vs AOT • 実行時プロファイリングが必要ない • 実行時メモリコストが減る •
動的コンパイルに必要なメモリが不要 • メモリ最適化が実行時コストを払わず行える • 最適化戦略がプログラマ任せ • JITは実行時プロファイリングを元に自動的に最適化してくれる • AOTでもProfile-guided optimizationという似た手法がある • プログラムへの理解度が低いほど最適化がしづらい • De-optimizationという謎テクニックが不要(当然対価は払うが)
ざっくりとした歴史 • 詳しくは是非C++標準化委員会の方にきいてください! • C++のtemplate機能の導入時に整数値を渡せることから発見される • Boost C++ Librariesなどで、テンプレートメタプログラミングとして発 達する
• C++11で、constexprという機能が導入される • templateを使わずともコンパイル時計算が可能に • C++14で、constexprの制限が緩和、使いやすくなった • より、C++らしい文法でコンパイル時計算が記述できるように • 現状、C++でおすすめの規格はC++14
template時代 • テンプレートメタプログラミング未満くらい(?) • 型から、効率的なデータ構造を作成するのによく用いられた • 顕著なのが: boost::array<int, 10> •
C++11以前の話なので、stdではない • 言語によっては、可変長配列などでやってしまう • ::size()というメンバ関数があるが、単に定数を返す(この場合10) • 配列の大きさを構造体メンバに含める必要がない • 実行時のチェックが減る • ヘッダーのみライブラリによる移植性
静的ダックタイピング • 継承とは別の形態の多態を行える • メソッド名とインターフェースを満たすならば同じ型として扱える • OCamlとかだと、Structural Subtyping(構造的部分型)とも • 多相型が使えてメソッドを静的解決する言語で使える
• 自由度が高すぎるため、制限されていく方向に • Rustのtrait • C++のConcept(C++20には入るらしい) • 他、型の型である「型クラス」に類するもの
C++11以前のTMP時代 • Boost C++ Librariesで盛んな印象 • templateの多用などで可読性が悪い • いわゆる、「型ベース」といわれるものが主体 •
整数演算がコンパイル時ですでにできたので、enumを駆使したり • ヘッダーのみライブラリの一般化 • templateを多用するため動的ライブラリとして型を決定できなくなった • ヘッダーファイル読み込みのI/Oでさらなるコンパイル時間の増加 • 具体的な型を書かずに済むような方向へ発展
C++11時代 • 長きC++の停滞を打破 • constexprが言語機能として実装された • コンパイル時レイトレーシングをする人がいた • 実質return文だけの関数しか定義できなかったので不便だった •
関数型プログラミングっぽさを強制はされた • constexprをコンパイル時に実行したときに例外を投げるとコンパイル 時エラーに • 詳しくは”Sprout”、中3女子、ボレロさんあたりでぐぐってください
C++14時代 • 定数式関数外に副作用が漏れなければ、関数内で副作用を許容 • 定数式内にとどまるなら、変数への再代入ができる • 複数の文を関数内で書くことができるように • Boost.Hana による「値ベース」のメタプログラミング
• C++03の実行時表現みたいなメタプログラミングが可能に • 定数式プログラミングが一定の完成をみた • ECMAScript 2015と同じで、黒魔術的なテクニックやツールを使わず とも標準機能で満足な(メタ)プログラミングが • 具体的な型を書く機会が減ってとてもうちやすい
サンプル:CTFEで階乗を計算するだけ • 長いので: https://wandbox.org/permlink/gTW0kZuDc1ffB3mh • 上からC++98, C++11, C++14っぽい書き方で • 古い書き方でもちゃんとコンパイルできるのがC++のよいところ
• C++17っぽい書き方は…知らぬ • Wandboxとは? • 無料で使えるオンラインコンパイラ(スポンサー募集中みたいです) • いろんなバージョンが揃っているのでバージョン依存の検証などに • C++に強い方が作ってるのでC++コンパイラがとても充実している • スクリプト言語も揃っている(Rubyもmrubyもある)
うれしくないところ • 実行時計算からは外れた書き方をする(C++14で改善) • エラーに慣れるまでわかりづらい • 実行時計算とのトレードオフで、コンパイル時間が長くなる • 結果、開発サイクルが遅くなる •
コンパイル時計算に対応したライブラリがあるとは限らない • 規格や流行の時期によって、書き方がかなり変わる • constexprの実現にはC++インタプリタが必要
うれしいところ • コンパイラの最適化に頼らずとも意図した最適化を行える • 実行時のオーバーヘッドを減らせる • コンパイル時レイトレーシングが行える • より副作用を意識できるようになる •
メンバ関数のconstも大事 • 実行時かコンパイル時に決定するかの観察眼が身につく
本題 • 今までのはC++的な歴史背景 • Rubyでどうコンパイル時計算を実現するのか • Rubyをコンパイル型にする道(?) • メソッド解決問題 •
実行時とコンパイル時の分離
Rubyのメタプログラミング • すべてが実行時に行われる • C++にあるコンパイル時制約がないので、自由に行える • メタプログラミングをRubyの表現から離れずに行える • もはや、Rubyの表現となっているためC++ほど違和感がないとも •
C++だとコンパイル時に行えることが実行時にしか行えない • やりすぎるほど効率が落ちてく印象 • メソッド定義をevalなどで上書きするなどの方法はあるが可動性は低下 • 便利さと効率がトレードオフなのは嬉しくない
現在のRubyのコンパイル方式 • 詳しくは Compiling Ruby を • 大体、以下の4つの形態をとって最後はVMに投入される • スクリプト(生のソースコード)
• トークン列 • 抽象構文木 • YARVコード(通称ISeq(Instruction Sequence)など) • YARVコードですらまだかなり実行時処理を残している • コンパイルする言語で静的に行うクラス・メソッド・定数の定義など • コードカバレッジでメソッド定義すらカバーされる
メソッドが動的解決される • メソッドの再定義がどこまでも許容されている • クラス定義時は型がかなり流動的である • 型チェック導入の高いハードル • 最終的に呼ばれるCの拡張メソッドの型はかなり静的 •
Object型として扱うとしても要求する振る舞いがある • すべてのクラス・メソッド・定数がVM上に読み込まれて、main関数の 実行を残すのみになると再定義したいケースが少ない • pryなどで利用したいケースもあるが、リリース後はほぼしない…はず • Rubyのオーバーヘッドでよくやり玉に
不変な実行環境 • クラス、メソッド、定数の変更を特定の地点から禁止する • 特定の地点からメソッドを静的解決できる • プログラムを定義部と実行部を分離する • すべてのライブラリを読み込んだ状態のVMをmain関数から最適化していく •
必要なgemをすべて読みこんだら実行環境を変えることはそうない • メソッドスコープ内だけが実行部(?) • Truffle Ruby と相性良さそう • 不変オブジェクト化することで型チェックもより容易に導入 • 開発時の利用は特に想定しない
基本方針 • 全モジュール読み込みなどの定義部をコンパイル時計算とみなす • 全モジュールの読み込みが終了した後以下の変更を禁止する • メソッド • クラス •
定数 • 固定化するオブジェクトの参照透過性 • 今までのRubyではできなかった最適化を行う • メソッドが静的に決まれば部分実行もできる • main関数などのエントリポイントの追加
実装方法(?) • ASTとYARVの間くらいで、全モジュール読み込みをはさむ • いくつか考えられる方式: • Ruby1.8時代のようにAST実行を行ったクラス情報を元にコード生成 • VM上で定義部を実行してmain関数から部分実行して行くメタプログラミング •
上の2つのハイブリッド方式 • 実行時とコンパイル時の分離 • Marshal.dump/loadできないオブジェクトは実行時オブジェクト? • 細かいところでとても紛糾しそう… • 場合によっては定数からクラスメソッドになるものも?
部分実行 • 定数式扱いできそうな部分をコンパイル時計算したい • いくつか制約を設ける • メソッドの入力が定数 • メソッドが変更可能なデータ(frozenでない)を参照しない •
そもそもランタイム実行とマークされていない • 基本的に `def` スコープの外だとほぼ実行可能? • classやmoduleスコープはほぼコンパイル時評価にできる • TypeErrorやArgumentErrorやNoMethodErrorのコンパイル時判定
ともあれ検証:Rails • とりあえず、Railsで性能が出るとウケるらしいので • やったこと • Rubyにメソッドの追加定義を禁止する改変 • 雑な定義したRailsをeager_loadで実行 •
WEBサーバーが立ち上がる直前くらいでメソッドの追加定義禁止 • Pumaのsingle.rbをいじる感じで • 困ったりするのか実証
メソッド定義禁止の方法とか • パッチ: • https://github.com/take- cheeze/ruby/commit/927b47d8e1fb76c347189265b76349658534105c • `rb_add_method` と `rb_const_set`
でフラグによって例外を投げれば 簡単にできた • ただマゾぷれいするだけなので…
結果:Rails • とりあえず「大規模なコード改修を入れないと無理」とだけ • eager_load しても読み込みが遅延されるクラスは残る • autoload はとても高いハードル •
requireで上書きしてもいいが… • そもそも、Railsのモジュール設計がautoload前提なので、require追加が大変 • 嬉しいことに廃止予定らしい: https://bugs.ruby-lang.org/issues/5653 • メソッドの遅延定義による最適化と相性が悪すぎる • 普通のwebページを出すところまでいけず…
もう一個検証: Hanami • 方法は大体一緒 • なんか流行っているらしいので • autoloadに依存しない構造なのでRailsほどひどくはなかった • http_router
でこけまくる • route をevalでコンパイルして最適化するため • こちらも普通のHTTPの200なページを出すところまでいけず • Rack難しかった…
さてどうするのか… • HanamiでもRailsでもそれほど有用でなさそう(当面は) • ただ、CRuby の開発者たちはマクロ自体に興味があるらしい • マクロを導入する場合は、表現をしっかりわかるべき派が強い印象 • メソッドスコープやブロック外だと大体マクロと変わらんと思うけど…
• Emacsのportable dumper的なのを実装する? • Productionだとコードをライブで編集するということをしないので
他あったら嬉しい方向性とか • Ruby as a VM optimizer • 最適化されたVMを生成するための言語としてのRuby •
クラス・メソッド・定数定義ですら評価(eval)してる言語 • Emacs Lispにできて我々にできないわけがないのでは? • キーワード引数の最適化 • キーワードの線形探索やハッシュ探索が不要になる • メソッド静的解決できると普通のメソッドと同等のコストで呼べるように • 人類に2引数以上のメソッドは早すぎる
とりあえず結論とか • JIT以外のナウくない高速化方法はある • コンパイル時計算!!!(これだけ覚えてってください!) • メソッドの静的解決(どちらかというと前提条件) • Rails難しい •
できることなら一つの言語で両方解決したい • 開発中は動的な方がやっぱりいい • 運用からはうまいこと静的な方向に行きたい(テストでつらい) • golang とてもいいけど、もっと言語内でメタプロしたい • C++の構文(反面教師として以外)以外は見習うところが多い
おまけ:Boost.Hanaの中のRuby • Boost.Hana内のC++コード生成にerbが使われている: • https://github.com/boostorg/hana/blob/e507494b5f7f51026e8afa317b3ca2c 058a4d290/include/boost/hana/detail/struct_macros.hpp.erb • コード生成はやはりスクリプト言語の十八番 • コード生成を構造的に簡単にできたらいい
おまけ:Rust • 今回は、あえて触れず • traitとかがキレイなので、Rubyに導入するのが難しそう • Rustはより強い制約を持つので静的ダックタイピングが難しそう • 個人的にC++を学んだ後にやる言語だと思ってる •
別の関数型言語のバックグラウンドがあればやってもいいとは思う • 言う人に言わせると未来のC++らしい • C++も当然発展していて新しい規格でかなり使いやすいので
おまけ:Swift • Appleの現在の主力言語 • 中身はObjective-Cのランタイムベース • Objective-Cはメッセージパッシングなオブジェクト指向なのでとても動的 • それでも静的型付け •
メモリ管理は、参照カウント • ただ、コンパイラが不要なカウント操作を削っていたり • mrubyの参照カウント版は実装したことが… • 動的なランタイムの上でも静的なチェックは有効である • JVMとかJavascriptとか
おまけ:Golang • RustとSwiftに触れてしまったので • バランス全振りな言語 • 妥当すぎる設計するのでつまらないときはあるが見習いたい • コード生成とかも自前でやってたりしてなかなかすごい •
パーサーとかもGolangで実装してある • reflectつよい • ほしい機能がだいたい標準で実装済み • システムプログラミングを謳うだけあって依存の少ないバイナリ • Rubyも夢のself-hostingできるようになってほしい…!
おまけ:mruby • 実は、今日話したことが部分的に既にできる • 依存モジュールだけを読み込むVMは作れる • 凝った最適化まではしていないし、不変でもない • 依存するrubyスクリプトはmrubyバイトコードにコンパイルはされる •
イマイチ影の薄い機能… • 引数をC側で受け取る時に緩めな静的型付けを行える • 本当は、静的シンボルテーブルとか静的クラス階層にしたい… • スクリプト言語の仮想マシンバイトコードのpeep-hole最適化つらたん • Golangに負けたくはない(謎の対抗心
おまけ:Erlang • 実行環境の話なので、Elixirに置き換えても可 • マクロはあるらしい • 関数型言語は、自分の足を撃つ確率が低そうで羨ましい • メモリ戦略がかなりシビアでよく考えられている •
耐障害性とかプロセス(という名の軽量スレッド)つよい • 参照透過性のおかげで実装の中で共有して使っても問題ない部分 が多いのが羨ましい