コンパイルキャッシュでRustのビルド時間を短縮しよう

これはRustその3 Advent Calendar 2019の初日の記事です。

## sccache &mdash; Mozillaが開発したRust製のコンパイルキャッシュ

Rustはコンパイル言語です。個人的にはRustのコンパイル速度は遅くはない方だと思っていますが、依存しているクレートが多いとビルドにかかる時間が長くなります。特に非同期I/Oを行うWebクライアント／サーバーのクレートを使ったときや、Cargo自体をライブラリとして使うCargoサブコマンドをビルドするときなどは、依存クレートが200個くらいになることがあり、マシンの性能によってはビルドにかなりの時間を要します。

ビルド時間を短縮するためにコンパイルキャッシュという種類のソフトウェアがあります。これはコンパイルによって作られた成果物をディスクなどにキャッシュしておき、同じ条件のコンパイル要求があったときには、キャッシュしたものを返してくれるというものです。CやC++向けですと [ccache][ccache] が有名です。

Rustで使用できるものには、Mozillaの [sccache][sccache]（Shared Compilation Cache、共有コンパイルキャッシュ）があります。これはccacheに似たコンパイルキャッシュ実装で、以下のような特徴があります。

[ccache]: https://ccache.dev/ 
[sccache]: https://github.com/mozilla/sccache

- ccacheと同様にgccやclangを使ったCやC++コンパイルに対応
- [rustcによるコンパイル][rustc-support] にも2016年の終わりごろから対応
    - 一応、3年経ったいまでも実験的というステータス？
- Rustで実装されている
- Linux、macOS、Windowsをサポート
- キャッシュの保存先としてローカルだけでなくクラウドストレージもサポート
    - ローカルディスク
    - Amazon S3とその互換ストレージ
    - Google Cloud Storage（GCS）
    - Microsoft Azure
    - Redis
    - Memcached
- ビルドサーバーをサポート（[docs/Distributed.md][docs-dist]、[docs/DistributedQuickstart.md][docs-dist-quick]）
    - 共有のLinuxサーバー群を使ってコンパイルし、結果をキャッシュする
    - チーム開発で便利
    - Linuxクライアントだけでなく、macOS、Windowsクライアントを対象としたクロスコンパイルも可能
    - クライアント認証をサポート
    - Mozillaの各オフィスで導入されている（出典：[Mozilla Source Tree Docs][source-tree-docs]）

[rustc-support]: https://github.com/mozilla/sccache/blob/master/docs/Rust.md
[docs-dist]: https://github.com/mozilla/sccache/blob/master/docs/Distributed.md
[docs-dist-quick]: https://github.com/mozilla/sccache/blob/master/docs/DistributedQuickstart.md
[source-tree-docs]: https://firefox-source-docs.mozilla.org/build/buildsystem/sccache-dist.html

sccacheを使ってRustプロジェクトをビルドすると、2回目以降のビルドが速くなります。ただしリンクにかかる時間は変わらないので、依存しているクレートが少ないと、あまり効果が得られないかもしれません。

sccacheについては @hhatto さんが2年前に [紹介記事][hhatto-sccache] を書かれてます。

[hhatto-sccache]: https://qiita.com/hhatto/items/7b0d6b31a0467086d0f3

今回はその内容と被るところが多いですが、ローカルディスクを使ったシンプルな構成のみ紹介します。最初にインストール方法とRustからの使い方を説明し、そのあと、私がその構成で半年近く使った感想などを簡単に書きます。

## sccacheのインストール

sccacheをインストールするには、Rustがインストール済みの環境で`cargo install sccache`を実行します。

インストールできたらsccacheのヘルプを表示してみます。

```console
$ sccache -h
sccache 0.2.12

USAGE:
    sccache [FLAGS] [OPTIONS] [cmd]...

FLAGS:
        --dist-auth       authenticate for distributed compilation
        --dist-status     show status of the distributed client
    -h, --help            Prints help information
    -s, --show-stats      show cache statistics
        --start-server    start background server
        --stop-server     stop background server
    -V, --version         Prints version information
    -z, --zero-stats      zero statistics counters

OPTIONS:
        --package-toolchain <executable> <out>    package toolchain for distributed compilation
        --stats-format <stats-format>
            set output format of statistics [default: text]  [possible values: text, json]


ARGS:
    <cmd>...    

Enabled features:
    S3:        true
    Redis:     false
    Memcached: false
    GCS:       false
    Azure:     false
```

ローカルディスクを使うなら、sccacheの設定は不要です。

## Rustから使ってみる

Rustからsccache使うにはRust 1.18からサポートされている`RUSTC_WRAPPER`環境変数を使います。

```console
$ export RUSTC_WRAPPER=$(which sccache)
```

あとは普通にCargoでビルドするだけです。

## 本当に速くなるか計ってみる

Actix-Webを使ったパッケージ（Rustプロジェクト）で試してみました。

環境は以下のとおりです。

- Rust 1.39.0
- sccache 0.2.12
- OS: Fedora 31
- CPU: Core i5-7200U @2.5GHz（低消費電力のラップトップ級CPU）

Rustパッケージ（`src/*.rs`や`Cargo.toml`）を用意します。

```console
# 書籍『実践Rust入門』のサンプルコードを使用する
$ git clone git@github.com:ghmagazine/rustbook.git
$ cd rustbook/ch11/templates

# 依存クレートを確認
$ tail -5 Cargo.toml
[dependencies]
actix-web = "0.7.16"
tera = "0.11.20"
serde_derive = "1"
serde = "1"
```

このパッケージのビルドにかかる時間を測定します。とはいえクレートをダウンロードする時間は計りたくないので、まずはsccacheなしで`cargo check`を実行しました。

```console
$ unset RUSTC_WRAPPER
$ cargo check
  Downloaded serde_derive v1.0.83
  Downloaded actix-web v0.7.16
  Downloaded sha1 v0.6.0
  ...
```

また私の環境では、sccacheによって過去のビルドの成果物がキャッシュされているので、それを削除しました。

 ```console
# 統計情報を表示して、キャッシュの場所を確認する
$ sccache --show-stats

..（中略）..
Cache location                  Local disk: "/home/tatsuya/.cache/sccache"
Cache size                            3 GiB
Max cache size                       10 GiB

# sccacheのサーバープロセスを停止する
$ sccache --stop-server

# キャッシュのディレクトリを削除する
$ rm -rf $HOME/.cache/sccache

# 統計情報を再度表示して、cache sizeがゼロになったことを確認
$ sccache --show-stats

...
Cache location                  Local disk: "/home/tatsuya/.cache/sccache"
Cache size                            0 bytes
Max cache size                       10 GiB
```

なおsccacheのサーバープロセスはCargoからのコンパイル要求があったときに自動でスタートします。また、最後のコンパイル要求から10分が経過したら自動的に停止します。

sccacheなしとありでビルドします。

```console
$ pwd
/home/tatsuya/ ... /rustbook/ch11/templates

# sccacheを使わずにビルド

$ unset RUSTC_WRAPPER
$ cargo clean
$ cargo build            # デバッグビルド
$ cargo clean
$ cargo build --release  # リリースビルド

# sccacheを使ってビルド

$ export RUSTC_WRAPPER=$(which sccache)
$ cargo clean
$ cargo build            # デバッグビルド
$ cargo clean
$ cargo build --release  # リリースビルド
```

sccacheを使ったビルドの方はデバッグビルドとリリースビルドのそれぞれについて、3回ずつ測定しました。結果は以下のとおりです。

**デバッグビルド**

| sccacheの有無 | ビルドの内容 | 所要時間 | sccacheなしが基準の相対速度 |
|:--:|:--|:--:|:--:|
| なし | デバッグビルド | 2m 01s | 1.00 |
| あり | デバッグビルド 1回目 | 2m 24s | 0.84 |
| あり | デバッグビルド 2回目 | 0m 36s | 3.36 |
| あり | デバッグビルド 3回目 | 0m 37s | 3.27 |

**リリースビルド**

| sccacheの有無 | ビルドの内容 | 所要時間 | sccacheなしを基準にした相対速度 |
|:--:|:--|:--:|:--:|
| なし | リリースビルド | 4m 59s | 1.00 |
| あり | リリースビルド 1回目 | 5m 13s | 0.95 |
| あり | リリースビルド 2回目 | 1m 11s | 4.21 |
| あり | リリースビルド 3回目 | 1m 09s | 4.33 |

まとめると以下のようになります。

- なにもキャッシュされていない状態では、sccacheありのほうが、なしよりも5%から15%遅くなった
- 2回目以降のビルドでは、sccacheありのほうが、なしよりも3.3倍から4.3倍速くなった

このまま別のRustパッケージもビルドしてみます。同じバージョンのActix Webを使用しますので、1回目のビルドからキャッシュの効果を期待できます。

```console
$ cd ../start-aw
$ tail -4 Cargo.toml
[dependencies]
actix-web = "0.7"
serde = "1"
serde_derive = "1"
```

リリースビルドのみ実行しました。結果は以下のとおりです。

| sccacheの有無 | ビルドの内容 | 所要時間 | sccacheなしを基準にした相対速度 |
|:--:|:--|:--:|:--:|
| なし | リリースビルド | 4m 18s | 1.00 |
| あり | リリースビルド 1回目 | 2m 16s | 1.90 |
| あり | リリースビルド 2回目 | 1m 06s | 3.90 |
| あり | リリースビルド 3回目 | 1m 06s | 3.90 |

予想どおり、1回目からある程度高速化しました。

キャッシュのヒット率などの統計情報を表示してみました。

```console
$ sccache --show-stats
Compile requests                   1698
Compile requests executed          1407
Cache hits                         1041
Cache hits (Rust)                  1041
Cache misses                        366
Cache misses (Rust)                 366
Cache timeouts                        0
Cache read errors                     0
Forced recaches                       0
Cache write errors                    0
Compilation failures                  0
Cache errors                          0
Non-cacheable compilations            0
Non-cacheable calls                 291
Non-compilation calls                 0
Unsupported compiler calls            0
Average cache write               0.000 s
Average cache read miss           2.660 s
Average cache read hit            0.001 s
Failed distributed compilations       0

Non-cacheable reasons:
crate-type                          264
-                                    27

Cache location                  Local disk: "/home/tatsuya/.cache/sccache"
Cache size                          323 MiB
Max cache size                       10 GiB
```

ディスクキャッシュは最大10GBまで保持する設定になっており、それを超えると、古いものから消されていきます。

## 半年近く使ってみた感想

ローカルディスクを使ったシンプルな構成のみで半年ほど使ってみました。

- sccache 0.2.8から0.2.11
    - キャッシュの保存先はローカルディスク
- Rust 1.35.0から1.39.0までのstableと、同時期のnightly（〜1.41.0）
- OS（x86_64系）
    - macOS 10.14 Mojave、10.15 Catalina
    - FreeBSD 12.0-RELEASE、12.1-RELEASE
    - Fedora 30、31

クラウドストレージやビルドサーバーは使っていません。というのは、私はリモート勤務なのでオフィスにいる同僚たちと離れているのと、ホームオフィスのマシンも1台ずつOSが異なるためです。

sccacheは基本的にいつもオンにしています。ただしRustコンパイラ（GitHub [rust-lang/rust][rust-src]）をソースコードからビルドするときだけは、念のためオフにしています。

[rust-src]: https://github.com/rust-lang/rust

使っていて遭遇したトラブルやsccacheのアップグレード方法を紹介します。

### トラブルや注意点など

sccacheに関連するトラブルは2回だけありました。ある日、nightlyツールチェインをアップデートしたら、`rustc`が変なエラーで動かなくなってしまいました。最初はコンパイラがおかしいのかと疑ってましたが、実際には`rustc`のあるコマンドライン引数に変更があって、sccacheがそれに対応していなかったことが原因でした。普段はsccacheがトラブルフリーなので使っていることさえ忘れかけてしまい、少し悩みました。結局、sccacheを最新版にアップグレードしたところ、あっさり解決しました。（たしかissueも上がっていたと思います）

もう1つのトラブルは単に使い方の問題でした。上の件とはまた別のときにsccacheをアップグレードしたのですが、そのあと、sccacheのサーバープロセスを再起動することを忘れてしまいまったのです。どんなエラーだったか忘れてしまいましたが、`cargo build`に失敗し、間違いに気づきました。

なおsccacheの [docs/Rust.md][docs-rust] にRustから使う際の注意点がまとめられていますので、かならず目を通すことをおすすめします。特にsccacheが管理できない方法でコンパイル結果に影響を与えるようなクレートがあると問題が起きそうです。たとえば以下のようなことが書かれています。

- Values from `env!` will not be tracked in caching.
- Procedural macros that read files from the filesystem may not be cached properly

幸いにも私はこのようなことが原因となるトラブルに遭遇したことはありません。（気づいてないだけかもしれませんが）

こういう部分には少し不安がありますので、プロダクション用のビルドにはsccacheを使わない方が無難でしょう。開発とか一部のCIとかで使うのがいいと思います。

[docs-rust]: https://github.com/mozilla/sccache/blob/master/docs/Rust.md

### sccacheのアップグレード

crates.ioにsccacheの新しいバージョンが公開されたときは、私は以下のようにしてアップグレードしています。

**私のやりかた**

1. `RUSTC_WRAPPER`を指定したまま（つまりsccacheを使って）`cargo install -f sccache`を実行する
2. sccacheのサーバープロセス（古いバージョン）を停止する（`sccache --stop-server`）

このやり方が心配なら、sccacheを使わずに`cargo install`を実行してもいいかもしれません。

**もっと慎重なやりかた**

1. sccacheのサーバープロセスを停止する（`sccache --stop-server`）
2. `unset RUSTC_WRAPPER`
3. `cargo install -f sccache`を実行する

## まとめ

sccacheはローカルディスクで使うなら設定が不要でトラブルもほとんどなく、またビルドが確実に高速化するのでおすすめです。

ビルドサーバーを使う構成については、私には経験がないので何も言えません。ただ、Mozillaの各オフィスでは使っているようですので、実用レベルには達しているようです。開発者の人数が多く、ビルドに長い時間がかかるプロジェクトなら、高い効果を期待できそうです。