【Rust】なぜformat!は遅いのか？

ちょっと煽ったタイトルですが、Rustで文字列連結を行う`format!`マクロがパフォーマンスにちょっとした影響を与えていることを知ったので紹介します

## TL;DR

- `format!`マクロは使いやすいですが、リリースでコンパイルする際には手動で`String::with_capacity`と`push_str`を使う方が高速でした
- 理由は`format!`が汎用的なフォーマット機能を提供するために内部で複雑な処理を行っているためです

## format!マクロとは

Rustの`format!`マクロは、Pythonの`f-strings`やC#の文字列補間に似た、便利な文字列フォーマット機能を提供します。

```rust
let name = "Rust";
let version = "1.76";
let message = format!("Hello, {} {}!", name, version);
// 結果: "Hello, Rust 1.76!"
```

このようにプレースホルダー`{}`を使って変数を文字列に埋め込むことができ、コードの可読性が高まります。

## パフォーマンスの問題

しかし、単純な文字列連結においては、`format!`マクロは意外にも非効率です。

```rust
use std::hint::black_box;
use std::time::Instant;

fn concat_format(a: &str, b: &str, c: &str) -> String {
    format!("{a} {b} {c}")
}

fn concat_capacity(a: &str, b: &str, c: &str) -> String {
    let mut buf = String::with_capacity(a.len() + 1 + b.len() + 1 + c.len());
    buf.push_str(a);
    buf.push(' ');
    buf.push_str(b);
    buf.push(' ');
    buf.push_str(c);
    buf
}

fn main() {
    let now = Instant::now();
    for _ in 0..100_000 {
        let a = black_box("first");
        let b = black_box("second");
        let c = black_box("third");
        black_box(concat_capacity(a, b, c));
    }
    println!("concat_capacity: {:?}", now.elapsed());
    
    let now = Instant::now();
    for _ in 0..100_000 {
        let a = black_box("first");
        let b = black_box("second");
        let c = black_box("third");
        black_box(concat_format(a, b, c));
    }
    println!("concat_format: {:?}", now.elapsed());
}
```

このコードを `cargo run` すると`format!`を使ったほうが高速です。

```
concat_capacity: 15.715208ms
concat_format: 12.059167ms
```


しかし、`--release`モードで実行すると、以下のように結果は逆転します：

```
concat_capacity: 2.913209ms
concat_format: 9.320417ms
```

## なぜformat!は遅いのか？

`format!`マクロが遅い主な理由はいくつか考えられます：

1. **汎用性の代償**: `format!`マクロは単なる文字列連結だけでなく、数値フォーマット、パディング、アライメントなど様々な機能をサポートしています。この柔軟性のために内部処理が複雑になっています。

2. **メモリ割り当ての最適化不足**: `String::with_capacity`を使うと必要なメモリを一度だけ確保できるのに対し、`format!`は事前にメモリ容量を最適化できません。

3. **std::fmt機構のオーバーヘッド**: `fmt::Arguments`構造体は6つのポインタサイズで構成され、3つのスライスを参照するという複雑な構造になっています：
   - フォーマットプレースホルダの周りのリテラル部分を含む`&'static [&'static str]`
   - 引数を指す`&[&(ptr, fn_ptr)]`（基本的に`&[&dyn Display]`）
   - フォーマットオプションを含む`Option<&'static [FmtArgument]>`

この問題はRustコアチームも認識しているようです：

- `fmt::Arguments`構造体が比較的大きい（6ポインタサイズ）
- 単純なフォーマット文字列でも静的ストレージのコストが高い
- 一つのプレースホルダでも特殊なオプションを使うと全プレースホルダに大きなオプション配列が使用される
- シンプルな文字列引数の場合でも完全な`Display`実装が必要となる

## 代替ライブラリの紹介

標準ライブラリの`format!`以外にも、高速なフォーマットライブラリがあります：

- [ufmt](https://docs.rs/ufmt/latest/ufmt/): `μfmt`と呼ばれるこのライブラリは、公式ドキュメントによれば`core::fmt`よりも6〜40倍小さく、2〜9倍高速なパニックフリーの代替手段とされています。

`ufmt`の公式ドキュメントで記載されている設計目標は次のとおりです：

1. バイナリサイズと速度の最適化（コンパイル時間よりも優先）
2. 生成コードでの動的ディスパッチの排除
3. 最適化時にパニックするブランチがない
4. 可能な限り再帰を避ける

`ufmt`の公式ドキュメントに掲載されているARMコルテックスM3チップ上のベンチマークによれば、`write!("Hello, world!")`が154サイクルかかるのに対し、`uwrite!("Hello, world!")`はわずか20サイクル（約13%）で実行可能とされています。

ただし[未定義動作につながる可能性](https://github.com/japaric/ufmt/issues/30)が指摘されており、プロダクション環境での使用には注意が必要そうです。

## まとめ

「読みやすさ vs パフォーマンス」のトレードオフを理解し、用途に応じて適切な方法を選びたいですね。

GitHub Issue [#99012](https://github.com/rust-lang/rust/issues/99012)から、`std::fmt::Arguments`の改善に取り組まれているので今後のリリースで改善するかもしれませんね。

## 参考文献

1. [Reddit discussion: Why is the format! macro so slow for string concatenation?](https://www.reddit.com/r/rust/comments/1jmxoto/why_is_the_format_macro_so_slow_for_string/)
2. [Rust GitHub Issue #99012: Tracking issue for format_args!() performance improvements](https://github.com/rust-lang/rust/issues/99012)
3. [ufmt crate documentation](https://docs.rs/ufmt/latest/ufmt/)
4. [Rust PR #106824: Flatten/inline format_args!() and literal arguments](https://github.com/rust-lang/rust/pull/106824)
5. [Rust PR #101568: Experiment: fmt::Arguments as closure](https://github.com/rust-lang/rust/pull/101568)
6. [Stack Overflow: How do I concatenate strings?](https://stackoverflow.com/questions/30154541/how-do-i-concatenate-strings)