Rust のコンパイル時間を改善する

## 環境

- OS：Windows 10 Pro 1909
- CPU：Intel Core i5-8200Y
- RAM：8GB
- Storage：256GB SSD
- rustc：1.51.0-nightly (a62a76047 2021-01-13)  

## 動機

[How to optimise compilation times with Rust](https://lemmy.ml/post/50089) という記事を見かけたので、私の環境でも早くなるのか試してみました。

Jpeg 画像をアップロードするとリサイズ・圧縮して、Jpeg 画像をレスポンスするローカルサーバをテストプログラムとして作成します。Web サーバフレームワークは [warp](https://crates.io/crates/warp) crate、画像のリサイズは [image](https://crates.io/crates/image) crate、画像圧縮は [mozjpeg](https://crates.io/crates/mozjpeg) crate を使用します。このプログラムのコンパイル時間がどの程度改善するか確認してみましょう。（コンパイル時間はすべてインクリメンタルビルドの時間です。）

## オリジナルのコード

これが改善する前のコードです。

```
📁 test_server
├ Cargo.toml
└ 📁 src
   ├ main.rs
   └ index.html
```

```toml:Cargo.toml
[package]
name = "test_server"
version = "0.1.0"
authors = ["benki"]
edition = "2018"

[dependencies]
image = "0.23"
mozjpeg = "0.8"
tokio = { version = "1.1", features = ["full"] }
warp = "0.3"
```

```rust:main.rs
use image::{imageops::Lanczos3, GenericImageView, ImageError, ImageFormat::Jpeg};
use mozjpeg::{ColorSpace, Compress, ScanMode};
use tokio::task::JoinError;
use warp::{hyper::body::Bytes, reject::Reject, Filter};

#[tokio::main]
async fn main() {
    let index = warp::path::end().map(|| warp::reply::html(include_str!("index.html")));

    let comp = warp::post()
        .and(warp::path("comp"))
        .and(warp::body::content_length_limit(1024 * 1024 * 10))
        .and(warp::body::bytes())
        .and_then(compress);

    let routes = index.or(comp);
    warp::serve(routes).bind(([127, 0, 0, 1], 55555)).await;
}

#[derive(Debug)]
enum TestServerError {
    Image(ImageError),
    Join(JoinError),
    DataToVec,
    InvalidRequest,
}

impl Reject for TestServerError {}

async fn compress(data: Bytes) -> Result<impl warp::Reply, warp::Rejection> {
    // Request Header の終わりを探す
    let pos = data
        .windows(4)
        .position(|d| d.eq(b"\r\n\r\n"))
        .ok_or(TestServerError::InvalidRequest)?;

    // Request Header より後のデータ取得
    let data = data.get(pos + 4..).ok_or(TestServerError::InvalidRequest)?;

    // Jpeg のデコード
    let img =
        image::load_from_memory_with_format(data, Jpeg).map_err(|e| TestServerError::Image(e))?;

    // 800px より大きければ 800px 以下にリサイズ
    let img = if img.width() > 800 || img.height() > 800 {
        img.resize(800, 800, Lanczos3)
    } else {
        img
    };

    // リサイズ後の幅・高さ取得
    let width = img.width() as usize;
    let height = img.height() as usize;

    // RGB データを取得
    let data = img.to_rgb8().to_vec();

    // MozJpeg で圧縮
    let data = tokio::task::spawn_blocking(move || {
        let mut comp = Compress::new(ColorSpace::JCS_RGB);
        comp.set_scan_optimization_mode(ScanMode::AllComponentsTogether);
        comp.set_quality(60.0);
        comp.set_size(width, height);
        comp.set_mem_dest();
        comp.start_compress();
        data.chunks(width * 3).for_each(|b| {
            comp.write_scanlines(b);
        });
        comp.finish_compress();
        comp.data_to_vec()
    })
    .await
    .map_err(|e| TestServerError::Join(e))?
    .map_err(|_| TestServerError::DataToVec)?;

    // 圧縮したデータを返す
    Ok(warp::http::Response::builder()
        .header("Content-Type", "image/jpeg")
        .body(data))
}
```

```html:index.html
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>Test Server</title>
</head>

<body>
    <form action="/comp" method="post" enctype="multipart/form-data">
        <input type="file" name="data" size="80">
        <input type="submit" value="Submit">
    </form>
</body>

</html>
```

|          | オリジナル |
|:--------:|:--------:|
| Debug    | 11.1s    |
| Release  | 69.0s    |

ここからどれだけ早くなるか見ていきましょう。

## デバッグ情報の無効化

デバッグしないなら無効にしてしまいましょう。`Cargo.toml`に以下を追加するだけなので簡単です。

```toml
[profile.dev]
debug = 0
```

|             | オリジナル | デバッグ無効 | 改善 |
|:-----------:|:---------:|:----------:|:----:|
| Debug      |  11.1s    | 9.7s        | 12%  |
| Release    | 69.0s     | -           |-     |

## default-features 無効化

warp の使用例で tokio の設定が`features = ["full"]`となっていますが、今回のテストプログラムには不要な crate まで読み込まれてしまします。必要最小限の feature だけ指定しましょう。また、image crate もデフォルトで png、tiff、webp などのデコーダを読み込むのでそれらを無効化します。

```toml:Cargo.toml
[package]
name = "test_server"
version = "0.1.0"
authors = ["benki"]
edition = "2018"

[dependencies]
# image = "0.23"
mozjpeg = "0.8"
# tokio = { version = "1.1", features = ["full"] }
warp = "0.3"

[dependencies.image]
version = "0.23"
default-features = false
features = ["jpeg_rayon"]

[dependencies.tokio]
version = "1.1"
features = ["macros", "rt-multi-thread"]

[profile.dev]
debug = 0
```

|          | オリジナル | DF 無効 | 改善 |
|:--------:|:---------:|:------:|:----:|
| Debug    | 11.1s     | 9.1s   |  18% |
| Release  | 69.0s     | 45.8s  |  33% |

## コード分割

コードを crate に分割します。今回のテストプログラムではローカルサーバ部分（`main`関数）と、画像圧縮部分（`compress`関数）で分割してそれぞれを crate にしてみます。

```
📁 test_server
├ Cargo.toml
├ 📁 main
│ ├ Cargo.toml
│ └ 📁 src
│    ├ main.rs
│    └ index.html
└ 📁 compress
   ├ Cargo.toml
   └ 📁 src
      └ lib.rs
```

```toml:/test_server/Cargo.toml
[workspace]
members = [
    "main",
    "compress"
]

[profile.dev]
debug = 0
```

```toml:/test_server/main/Cargo.toml
[package]
name = "test_server"
version = "0.1.0"
authors = ["benki"]
edition = "2018"

[dependencies]
compress = { path = "../compress" }
warp = "0.3"

[dependencies.tokio]
version = "1.1"
default-features = false
features = ["macros", "rt-multi-thread"]
```

```rust:/test_server/main/src/main.rs
use compress::compress;
use warp::Filter;

#[tokio::main]
async fn main() {
    let index = warp::path::end().map(|| warp::reply::html(include_str!("index.html")));

    let comp = warp::post()
        .and(warp::path("comp"))
        .and(warp::body::content_length_limit(1024 * 1024 * 10))
        .and(warp::body::bytes())
        .and_then(compress);

    let routes = index.or(comp);
    warp::serve(routes).bind(([127, 0, 0, 1], 55555)).await;
}
```

```toml:/test_server/compress/Cargo.toml
[package]
name = "compress"
version = "0.1.0"
authors = ["benki"]
edition = "2018"

[dependencies]
mozjpeg = "0.8"
tokio = "1.1"
warp = "0.3"

[dependencies.image]
version = "0.23"
default-features = false
features = ["jpeg_rayon"]
```

```rust:/test_server/compress/src/lib.rs
use image::{imageops::Lanczos3, GenericImageView, ImageError, ImageFormat::Jpeg};
use mozjpeg::{ColorSpace, Compress, ScanMode};
use tokio::task::JoinError;
use warp::{hyper::body::Bytes, reject::Reject};

#[derive(Debug)]
enum TestServerError {
    Image(ImageError),
    Join(JoinError),
    DataToVec,
    InvalidRequest,
}

impl Reject for TestServerError {}

pub async fn compress(data: Bytes) -> Result<impl warp::Reply, warp::Rejection> {
    // Request Header の終わりを探す
    let pos = data
        .windows(4)
        .position(|d| d.eq(b"\r\n\r\n"))
        .ok_or(TestServerError::InvalidRequest)?;

    // Request Header より後のデータ取得
    let data = data.get(pos + 4..).ok_or(TestServerError::InvalidRequest)?;

    // Jpeg のデコード
    let img =
        image::load_from_memory_with_format(data, Jpeg).map_err(|e| TestServerError::Image(e))?;

    // 800px より大きければ 800px 以下にリサイズ
    let img = if img.width() > 800 || img.height() > 800 {
        img.resize(800, 800, Lanczos3)
    } else {
        img
    };

    // リサイズ後の幅・高さ取得
    let width = img.width() as usize;
    let height = img.height() as usize;

    // RGB データを取得
    let data = img.to_rgb8().to_vec();

    // MozJpeg で圧縮
    let data = tokio::task::spawn_blocking(move || {
        let mut comp = Compress::new(ColorSpace::JCS_RGB);
        comp.set_scan_optimization_mode(ScanMode::AllComponentsTogether);
        comp.set_quality(60.0);
        comp.set_size(width, height);
        comp.set_mem_dest();
        comp.start_compress();
        data.chunks(width * 3).for_each(|b| {
            comp.write_scanlines(b);
        });
        comp.finish_compress();
        comp.data_to_vec()
    })
    .await
    .map_err(|e| TestServerError::Join(e))?
    .map_err(|_| TestServerError::DataToVec)?;

    // 圧縮したデータを返す
    Ok(warp::http::Response::builder()
        .header("Content-Type", "image/jpeg")
        .body(data))
}
```

|          | オリジナル | コード分割 | 改善 |
|:--------:|:---------:|:--------:|:----:|
| Debug    | 11.1s     | 8.7s     |  21% |
| Release  | 69.0s     | 46.9s    |  32% |

## まとめ

|          | オリジナル | デバッグ無効 | DF 無効 | コード分割 |
|:--------:|:---------:|:----------:|:-------:|:---------:|
| Debug    | 11.1s     | 9.7s       |9.1s     |  8.7s    |
| Release  | 69.0s     |  -         |45.8s    |  46.9s   |

元記事と同じように 2～3 割のコンパイル時間の改善が見られました。今回のテストプログラムのような単純なものでもそれなりの改善が見られたので、もっと大きなプログラムだと恩恵は大きいかもしれませんね。Rust のコンパイル時間が改善されることを期待します。