ちょっと煽ったタイトルですが、Rustで文字列連結を行うformat!
マクロがパフォーマンスにちょっとした影響を与えていることを知ったので紹介します
TL;DR
-
format!
マクロは使いやすいですが、リリースでコンパイルする際には手動でString::with_capacity
とpush_str
を使う方が高速でした - 理由は
format!
が汎用的なフォーマット機能を提供するために内部で複雑な処理を行っているためです
format!マクロとは
Rustのformat!
マクロは、Pythonのf-strings
やC#の文字列補間に似た、便利な文字列フォーマット機能を提供します。
let name = "Rust";
let version = "1.76";
let message = format!("Hello, {} {}!", name, version);
// 結果: "Hello, Rust 1.76!"
このようにプレースホルダー{}
を使って変数を文字列に埋め込むことができ、コードの可読性が高まります。
パフォーマンスの問題
しかし、単純な文字列連結においては、format!
マクロは意外にも非効率です。
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!
マクロが遅い主な理由はいくつか考えられます:
-
汎用性の代償:
format!
マクロは単なる文字列連結だけでなく、数値フォーマット、パディング、アライメントなど様々な機能をサポートしています。この柔軟性のために内部処理が複雑になっています。 -
メモリ割り当ての最適化不足:
String::with_capacity
を使うと必要なメモリを一度だけ確保できるのに対し、format!
は事前にメモリ容量を最適化できません。 -
std::fmt機構のオーバーヘッド:
fmt::Arguments
構造体は6つのポインタサイズで構成され、3つのスライスを参照するという複雑な構造になっています:- フォーマットプレースホルダの周りのリテラル部分を含む
&'static [&'static str]
- 引数を指す
&[&(ptr, fn_ptr)]
(基本的に&[&dyn Display]
) - フォーマットオプションを含む
Option<&'static [FmtArgument]>
- フォーマットプレースホルダの周りのリテラル部分を含む
この問題はRustコアチームも認識しているようです:
-
fmt::Arguments
構造体が比較的大きい(6ポインタサイズ) - 単純なフォーマット文字列でも静的ストレージのコストが高い
- 一つのプレースホルダでも特殊なオプションを使うと全プレースホルダに大きなオプション配列が使用される
- シンプルな文字列引数の場合でも完全な
Display
実装が必要となる
代替ライブラリの紹介
標準ライブラリのformat!
以外にも、高速なフォーマットライブラリがあります:
-
ufmt:
μfmt
と呼ばれるこのライブラリは、公式ドキュメントによればcore::fmt
よりも6〜40倍小さく、2〜9倍高速なパニックフリーの代替手段とされています。
ufmt
の公式ドキュメントで記載されている設計目標は次のとおりです:
- バイナリサイズと速度の最適化(コンパイル時間よりも優先)
- 生成コードでの動的ディスパッチの排除
- 最適化時にパニックするブランチがない
- 可能な限り再帰を避ける
ufmt
の公式ドキュメントに掲載されているARMコルテックスM3チップ上のベンチマークによれば、write!("Hello, world!")
が154サイクルかかるのに対し、uwrite!("Hello, world!")
はわずか20サイクル(約13%)で実行可能とされています。
ただし未定義動作につながる可能性が指摘されており、プロダクション環境での使用には注意が必要そうです。
まとめ
「読みやすさ vs パフォーマンス」のトレードオフを理解し、用途に応じて適切な方法を選びたいですね。
GitHub Issue #99012から、std::fmt::Arguments
の改善に取り組まれているので今後のリリースで改善するかもしれませんね。
参考文献
- Reddit discussion: Why is the format! macro so slow for string concatenation?
- Rust GitHub Issue #99012: Tracking issue for format_args!() performance improvements
- ufmt crate documentation
- Rust PR #106824: Flatten/inline format_args!() and literal arguments
- Rust PR #101568: Experiment: fmt::Arguments as closure
- Stack Overflow: How do I concatenate strings?