0
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 1.97 で backtrace に型が出るようになったので、コストと一緒に実測した

0
Last updated at Posted at 2026-07-21

Rust 1.97.0(2026-07-09)で、シンボルマングリングのデフォルトが legacy から v0 に変わりました。
この記事は「バックトレースやツールの表示が実際どう変わるのか」「バイナリはどれだけ大きくなるのか」を、1.96 / 1.97 の実バイナリで測った記録です。
対象読者は、Rust バイナリを本番で動かしていて、panic やプロファイル結果を読む機会がある人に役立てばと。

1.97 で良くなるのは、ジェネリクスの具体型がシンボル名に残るようになることです。
その分シンボル文字列は +13〜32% 増えますが、リンク時間は測っても差が出ませんでした。
注意点が 1 つだけあって、release のデフォルト設定ではそもそもシンボルがバイナリに残りません。
設定を変えずに 1.97 へ上げても、nm の結果は no symbols のままです。

何が変わったのか

v0 マングリング自体は 1.59 から opt-in で使えました。
8 年かけてようやくデフォルトになりました。
見分け方は単純で、legacy は _ZN、v0 は _R で始まります。
実物を見た方が早いので、次のコードを両方のバージョンでビルドしました。

use std::collections::HashMap; // 検証用に HashMap を使う

#[inline(never)] // インライン化させず、シンボルとして必ず残す
fn lookup<'a, K: std::hash::Hash + Eq, V>(map: &'a HashMap<K, V>, key: &K) -> Option<&'a V> {
    map.get(key) // キーに対応する値への参照を返すだけ
}

fn main() {
    // lookup を <String, Vec<u32>> でインスタンス化させるための型
    let mut m: HashMap<String, Vec<u32>> = HashMap::new();
    m.insert("a".into(), vec![1, 2]); // 適当な値を 1 件入れる
    println!("{:?}", lookup(&m, &"a".to_string())); // ここで単相化が起きる
}

nmlookup のシンボルを取り出すとこうなります。

# 1.96.0(legacy)
_ZN11mangle_demo6lookup17hbb07f546efb3e6fbE

# 1.97.1(v0)
_RINvCsT2VuYQ1sEf_11mangle_demo6lookupNtNtCscdodAO9FK5_5alloc6string6StringINtNtBD_3vec3VecmEEB2_

legacy に残っているのは lookup という名前とハッシュだけ。
<String, Vec<u32>> でインスタンス化されたという情報は、ここからは復元できません。
v0 の方は 6String3Vecm(u32 の 1 文字符号)が読み取れます。
demangle を通すと差がはっきり出ます。

$ nm -C target/release/mangle-demo | grep lookup
# 1.96: mangle_demo::lookup
# 1.97: mangle_demo::lookup::<alloc::string::String, alloc::vec::Vec<u32>>

なお、標準ライブラリは 1.96 の時点ですでに v0 でビルドされていました。
今回変わったのはユーザークレート側です。
v0 の符号化仕様そのもの(backref 圧縮や Punycode)はこの記事では扱いません。
詳細は RFC 2603 に譲ります。

検証環境

  • WSL2(kernel 6.6.87.2)/ Ubuntu 24.04.4 / Intel Core i7-14700F
  • rustc 1.96.0 / 1.97.1 / 1.97.0-nightly(2026-04-29, c935696dd)
  • GNU binutils 2.42(nm / addr2line / c++filt)/ rustfilt

バックトレースはこう変わる

panic するジェネリック関数を用意して比べます。

#[inline(never)] // インライン化させず、シンボルとして必ず残す
fn parse_pair<T: std::str::FromStr>(s: &str) -> (T, T) // "a,b" 形式を (T, T) にパースする
where
    T::Err: std::fmt::Debug, // unwrap で表示するため Debug を要求
{
    let mut it = s.split(',');                        // カンマで分割
    let a = it.next().unwrap().parse::<T>().unwrap(); // 1 要素目をパース
    let b = it.next().unwrap().parse::<T>().unwrap(); // 2 要素目をパース(無ければここで panic)
    (a, b) // 2 つ揃ったら返す
}

fn main() {
    println!("{:?}", parse_pair::<u64>("1")); // 2 要素目がなく b 側の unwrap で panic
}

RUST_BACKTRACE=1 で実行して、該当フレームだけ抜き出すとこうです。

# 1.96.0
   5: mangle_demo::parse_pair
# 1.97.1
   5: mangle_demo::parse_pair::<u64>

parse_pair::<u64> — どの型で落ちたか、最初から書いてあります。
RUST_BACKTRACE=full にすると、legacy は parse_pair::he54dddcca06249cd とハッシュ付き、v0 は mangle_demo[cdf0d36d2cd29192]::parse_pair::<u64> とクレートの disambiguator 付き。
closure の表記も {{closure}} から {closure#0} に変わって、複数 closure の区別がつくようになりました。

実プロジェクトだともっと効きます。
ripgrep 15.2.0 を 1.96 / 1.97 でビルドして、同じ関数のシンボルを見比べると:

# 1.96: どのインスタンスか分からない
grep_searcher::searcher::core::Core<M,S>::count_lines

# 1.97: 具体型まで書いてある(長いので型引数の一部を ... で省略)
<grep_searcher::searcher::core::Core<&&grep_regex::matcher::RegexMatcher,
  &mut grep_printer::json::JSONSink<..., termcolor::Buffer>>>::count_lines

legacy が持っているのは定義時のパラメータ名 <M,S> だけ。
モノモーフィゼーション起因の性能問題(特定のインスタンスだけ遅い・コードサイズが増える)を追うとき、この差は初動の速さに直結します。

手元のツールは v0 を読めるのか

公式リリースノートは「古いデバッガ・プロファイラでは demangle に失敗しうる」と注意しています。
Ubuntu 24.04 の標準構成で試した結果は全勝でした。
nm -Cobjdump --demangleaddr2line -f -Cc++filtrustfilt、どれも v0 を正しく復元します(binutils 2.42)。
c++filt だけはクレートの disambiguator まで出す冗長形式でした。

perf は未検証です。
この検証環境(WSL・sudo なし)には perf を入れられませんでした。
【要実測:Linux 実機か sudo の使える環境で perf recordperf report を実行し、v0 シンボルが demangle されるか確認。perf の v0 対応パッチは 2025-02 に入ったばかりのため、distro のバージョン次第で rustfilt を通す運用になる可能性が高い】

symbol-pipeline.png

サイズはどれだけ増えるのか

公式の説明は「v0 はシンボルが長くなり、サイズとリンク時間に軽微な影響がある」。
その「軽微」を数字にします。

コンパイラの世代差を混ぜたくないので、同一の nightly(1.97.0-nightly)でマングリングだけを切り替えました。
legacy の明示指定は nightly 限定で、将来消える予定のフラグです。

# マングリングだけ legacy に切り替えてビルド(この指定は nightly 限定)
RUSTFLAGS="-Csymbol-mangling-version=legacy -Zunstable-options" cargo +nightly build --release

測るのは 2 つ。
ジェネリック関数を 300 インスタンス化する合成クレートと、ripgrep 15.2.0 です。
どちらも strip = "none" でシンボルを残しています。

計測対象 legacy v0 差分
合成クレート .strtab 181,502 B 238,910 B +31.6%
合成クレート バイナリ全体 4,896,408 B 4,953,832 B +1.2%
ripgrep .strtab 1,216,395 B 1,376,470 B +13.2%
ripgrep .debug_str 7,163,333 B 9,011,208 B +25.8%
ripgrep .text 2,752,766 B 2,750,430 B −0.1%
ripgrep バイナリ全体 28,590,360 B 30,572,184 B +6.9%

ここで数字が合いません。
ripgrep のバイナリは 28.6MB → 30.6MB と 2MB 近く増えたのに、.strtab の増分は 160KB しかない。
残りはどこかと readelf -SW で両方のバイナリを比べると、.debug_str が +1.85MB でした。
ripgrep は release で debug = 1 にしているので、DWARF の linkage name にもマングル名が入って、そちらが増えていたわけです。
.text は実質同一。
コード生成には影響しません。
シンボルを strip して配布する構成なら、増分はゼロです。

意外だったのは最長シンボルです。
legacy 804 文字に対して v0 は 609 文字と、ここだけ逆転します。
v0 は平均こそ長い(合成クレートで 68.5 → 90.6 文字)ものの、一度出た path を参照で済ませる backref があるので、深いネストの最悪ケースはむしろ短い。

リンク時間は touch → 再ビルドを各 5 回実行して、v0 が 1154〜1183ms、legacy が 1159〜1183ms。
誤差の範囲です。

落とし穴 1:release デフォルトではそもそもシンボルが残らない

最初に nm を実行したら no symbols
ここで数分固まりました。
readelf -SW で見ると .symtab ごと無い。
cargo は debug=0 の release ビルドで -C strip=debuginfo を自動で渡していて、この環境ではこれで .symtab まで消えます(rustc を直接実行しても再現)。
つまり素の release ビルドでは、v0 の良さもサイズ増も成果物に現れません。
実測や運用でシンボルを使うなら strip = "none"debug = 1 の明示が前提。
プロファイルを読むなら、私は ripgrep に倣って debug = 1 にします。

落とし穴 2:インライン化されたフレームは v0 でも消える

#[inline(always)] の呼び出し連鎖の先で panic させると、dev ビルドでは level3::<u32> まで型付きで出ます。
release(debug=0)では main しか表示されなくなります。
v0 が改善するのはシンボルテーブル経由の名前解決だけで、インライン化で消えたフレームは debuginfo がないと復元できません。
ここを混同すると「1.97 に上げたのにバックトレースが読めない」と誤診します。
1.97 beta にはバックトレースがハッシュ表示になる回帰(#157432)がありましたが、stable 1.97.1 では再現しませんでした。

ここまでの切り分けを 1 枚にまとめるとこうなります。

backtrace-triage.png

おまけ:リンカ警告の可視化に早速引っかかった

1.97 のもう一つの変更「リンカの stderr を警告として表示」には、この検証の最中に自分が引っかかりました。

warning: linker stderr: ignoring deprecated linker optimization setting '1'
  = note: `#[warn(linker_messages)]` on by default

この警告、1.96 まではリンカが出力しても rustc が表示せずに捨てていました。
[lints.rust] linker_messages = "allow" で元に戻せますが、私は出したままにします。
というか、リンカに deprecated な設定が渡っていたこと自体、この警告で初めて知りました…。

所感

v0 のデフォルト化は地味です。
ただ、障害調査の初動は確実に一段速くなる。
Core<M,S> としか出なかったフレームに具体型が入るだけで、当たりを付けるための再現ビルドが 1 回減ります。
シンボル +13〜32% は、シンボルを残すと決めた時点で織り込むコストの範囲でしょう。

0
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
0
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?