Rustの最適化は保証はないがアテにできる

κeenです。Rustコンパイラは最適化が行われることの保証をしませんが、実用的にはアテにしていいんじゃないかという話です。

発端はこのツイート。

それに私が反応して、少し議論がはじまりました。

その内容をまとめておきます。

まず、冒頭に書いたとおりRustコンパイラがXXという最適化をする、といった仕様はないはずです。そもそも言語仕様も最近までない言語でしたしね。じゃあ保証がないから実際に行なわれないのかというとそんなこともなく、 -O オプションをつけると様々な最適化がされます。

こういった最適化に依存してコードを書けると楽になるケースというのが多々あります。引用したツイートにある末尾呼出(再帰)の最適化(TCO)なんかが有名ですかね。再帰で書いたコードがTCOされないとスタックオーバーフローしてしまうのでこれがあるかないかでコードの書き方が変わってしまいます。ところが最適化される保証がないので、安全側に倒すと最適化されない前提で再帰をしないコードを書かざるをえません。実際には最適化が効くケースもあるのに使えないのは歯痒いですね。

ここで、どうしてRustコンパイラが最適化をする保証がないのか考えてみます。探せばどこかに書いてあるかもしれませんが、私が思うに

  1. そもそも言語仕様もなかったし、最適化の保証もあんまりやる気がない
  2. デバッグビルドではコンパイル速度を重視したいので最適化を必須にしたくない
  3. -O オプションありの話でも最適化をほとんどLLVMに任せているので保証しづらい

なんかの理由が考えられます。

このうち2、3あたりは状況次第なところがあります。

2は例えば無用コード除去(Dead Code Elimination)なんかをするとコンパイル対象が減るのでコンパイル速度が速くなる可能性が高くなります。これはデバッグビルドでも取り入れたいですよね。あるいは、ジェネリクスの単相化のように最適化というよりコードをコンパイルするための必須のプロセスもあります。TCOもスタックオーバーフローせずに動かすための必須なプロセスと考えれば言語仕様に組込む余地がありそうですよね。余談ですが実際にSchemeやStandard MLといった関数型言語ではTCOが言語仕様で必須となっています。Rustは先のツイートにも書いたように言語仕様的にTCOを入れづらいのですが、そういったコンパイル結果についての仕様があるとうれしいですよね。

3は最適化を「ほとんど」LLVMに任せていると書いたとおり、多少はRust側でやってる最適化もあります。Rustは中間言語にMIRというコントロールフローグラフを持っていて、ここでよくある最適化はできるようになっています。LLVMでできる最適化をここで労力かけてやる意味はないのでRustに固有の最適化や、先に挙げたDCE系のように先にやっておくとコンパイル時間が削減できるものに限りますが、ある程度Rust側でやっている最適化もあるのです。コンパイラのコードを読むとここらへんにpassがまとまって書かれているのが分かります。

compiler/rustc_mir_transform/src/lib.rs#L702

あるいは、LLVMのやる最適化はRustで制御しづらいといってもRustコンパイラがLLVM IRを吐くときに気をつけて最適化されやすいIR列を吐いてあげれば事実上最適化が確定しそうですよね。実はRustコンパイラはそういった意識をしているらしく、Rustのテストファイルにはコードを変換した結果のLLVM IRにXXという命令が含まれる、あるいは余計なYYという命令が含まれない、などを検査するテストがあります。

tests/codegen-llvm/constant-branch.rs

さらに言えばものによっては実アセンブリまで吐かせて望ましいアセンブリが出ていることを確認するテストも存在します。

tests/assembly-llvm/simd/reduce-fadd-unordered.rs

少なくとも偶然ではなくコンパイラ開発者が意図した動作であるという意味では、これらの場所に書かれている最適化はアテにできるんじゃないでしょうか。

Written by κeen