backend を正当化する痛みを探して

Mere は四つの backend を持ち、その一つには明確な仕事がなかった。LLVM backend は C のものと重複していた:native コードはすでに C を通して手書き C の速度で走り、web は WebAssembly で走った。だから勘で消すのでも信仰で保つのでもなく、問いは言語がすべてを答えるやり方で答えられた —— それを強いる具体的な痛みを探しに行く。三つの probe が探しに出て、それぞれ正当化を見つけると予期し、それぞれ溶けた。backend が要らないという発見はそれ自体持つに値し、probe の一つが、探していなかった場所で、後に効く真実を掘り当てた。

merellvmbackendsjitdogfoodlanguage-design

四つの backend を持つ言語は、それぞれが何のためかを言えるべきだ。Mere の三つは易しく答えた: interpreter は速く反復するため、C backend は手書き C の速度で native バイナリを生み、WebAssembly は web 上と Node ホストの下で走る。四つ目、LLVM backend は答えられなかった。それが生むものを何であれ、 C backend も生み、まったく同じ速さで。それは当然のこととして運ばれてきた、まじめな言語なら LLVM を ターゲットにすべきだと仮定するやり方で、そして静かに重複になっていた。

きちんとした選択肢は、それを消すか、原則で守り続けるかだった。どちらもこの言語がものを決めるやり方で ない。feature は実の現在の痛みを除くことでその場所を得るので、正直な一手は、LLVM backend を genuine に 必要なものへと強いる痛みを探しに行くこと —— そして答えが空で返ってきても信じること、だった。三つの probe が出て行った。

鏡だった千倍の gap

最初の probe は JIT コンパイラを欲しがる古典的な理由を追った:runtime でコードを受け取り、それを速く 走らせねばならないプログラム。式のツリーを一千万回評価するのに、interpreter で歩くと二十秒、同じ計算を native コードでまっすぐ書くと二十ミリ秒かかった。千倍の gap はまさに JIT 型の穴の形だ。

それから probe が dogfood された、ファイルの各行に対しユーザ供給の式を評価する awk 的なツールとして、 そして gap は崩れた。千倍は 二重解釈の産物だった —— Mere interpreter がそれ自身、式のツリーを歩く ためにツリーを歩いていた。ツールを前もって、普通のやり方でコンパイルすれば、その式評価器は native コードになる;runtime の式はそのとき本質的に native の速度で走り、JIT はどこにもない。仮説にとって もっと悪いことに、この種の実のツールは算術でなく入力と出力に縛られる:二百万行にわたって、式が単一の 列でも深くネストした式でも、wall-clock 時間は動かなかった、評価が parse と print のノイズに埋もれた から。JIT が和らげる痛みは、この種のプログラムが実際には持たない痛みだった。

一つの genuine な亀裂

二つ目の probe は実の理由に最も近いものを見つけ、それは genuine で同時に小さいがゆえに精密に述べる 価値がある。深い相互末尾再帰 —— 二つの関数が互いを一千万回呼ぶ —— は WebAssembly で完走し、最適化 つきでコンパイルした native バイナリで完走し、最適化なしでコンパイルした native バイナリでスタック オーバーフローでクラッシュした。WebAssembly は format のレベルで末尾呼び出し除去を保証する;C はせず、 それを C コンパイラの optimizer に任せ、それは高い最適化で畳み低いところで放棄する。C にはその除去を 要求する手段がない。native コードで末尾呼び出しを保証するには、それができる LLVM の中間表現を欲しく なるだろう。

これが LLVM が寄りかかる一箇所だ —— そしてここでさえ寄りは軽い、なぜなら推奨される native ビルドは 最適化つきで走り、そこでは末尾呼び出しは畳まれるから。保証が重要になるのは genuine にそれを要する ワークロードのためだけだ:継続の重い interpreter、状態機械、永遠に ping-pong するスタイルで書かれた proof checker。そういうプログラムが現れ optimizer のベストエフォートを不十分と見出すまで、それを保証 する機構は誰も訊いていない問いに答えている。だが flag するに値する、この probe が末尾呼び出しと保証に ついて何か真実を見つけていたこと —— そしてその真実が、probe が探していた native コードでなく、まったく 別のどこかで、後に効いてくること、を。

C が表現できないもの

三つ目の probe は問いまるごとを再構成し、その再構成が実の結論だ。使っている C コンパイラはそれ自身 LLVM の上に建てられている。だから LLVM が有名なすべて —— その optimizer、その vectorization、その link-time の仕事 —— を、C backend はすでにただで継承している、そのコンパイラを通してコンパイルする ことで。LLVM backend は、C backend が まったく表現できない何かをすることでしか、その糧を稼げず、 探索はそれが何でありうるかに絞られた。

最も有望に見えた候補は coroutine —— 計算を中断し再開する能力 —— で、resumable な effect がそれを 要求するという理屈だった。だが Mere の effect は普通の capability 値として渡される;algebraic effect handler も、resume も、yield も、中断するものは何もなく、だから需要はその方向から単に生じない。 そして人が実際に欲しがる中断のパターン —— 遅延ストリーム、generator —— は普通の closure と thunk で 表現でき、C を通して native にコンパイルされ何事もなく、有限の答えに合計された無限ストリームで検証 された。LLVM が代替不能だろう genuine なケースは、C が真に届けないもの —— GPU カーネルを emit する、 tracing ガベージコレクタを走らせる —— で、そのそれぞれが言語が何であるかの変更、Mere がしていない 転換だ。そのどれも現在の痛みでない。

否定の価値

だから答えは空で返り、空が持つに値する答えだった。LLVM backend を存在へと強いる痛みはなかった、なぜなら 密かに LLVM である C コンパイラがすでに LLVM の最適化を届け、WebAssembly がすでに末尾呼び出しを保証し、 effect システムは中断しないから。backend はあるところに留まる —— 実験的、当面は重複 —— そしてそれに 投資しないという決定は肩をすくめることでなく、測られた発見で、そのトリガは名指されている:Mere が GPU コードを emit したくなる日、あるいはその effect が再開する能力を育て zero-allocation な状態機械に コンパイルしたくなる日、probe は違う結果で返ってくるだろう。

これは以前の Part がロックと atomic を建てるのを断ったときに実践したのと同じ抑制だ:建てないことを、 建てることとちょうど同じだけ、証拠を要する行為として扱う規律。三つの probe の成果は一つの文だった —— LLVM にまだ投資するな、そしてこれがその理由だ —— 好みでなく計測に裏付けられて。そしてその否定の内に 畳まれていたのは、保証された末尾呼び出しについての一つの小さな真実で、通りすがりに気づかれ、しまわれ、 コンパイラが自分自身をコンパイルしようとする物語の Part を待っていた。

← Back to Mere: 言語を作る