2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-03-30

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

TL;DR

  • format!マクロは使いやすいですが、リリースでコンパイルする際には手動でString::with_capacitypush_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!マクロが遅い主な理由はいくつか考えられます:

  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: μfmtと呼ばれるこのライブラリは、公式ドキュメントによればcore::fmtよりも6〜40倍小さく、2〜9倍高速なパニックフリーの代替手段とされています。

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

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

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

ただし未定義動作につながる可能性が指摘されており、プロダクション環境での使用には注意が必要そうです。

まとめ

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

GitHub Issue #99012から、std::fmt::Argumentsの改善に取り組まれているので今後のリリースで改善するかもしれませんね。

参考文献

  1. Reddit discussion: Why is the format! macro so slow for string concatenation?
  2. Rust GitHub Issue #99012: Tracking issue for format_args!() performance improvements
  3. ufmt crate documentation
  4. Rust PR #106824: Flatten/inline format_args!() and literal arguments
  5. Rust PR #101568: Experiment: fmt::Arguments as closure
  6. Stack Overflow: How do I concatenate strings?
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?