世界を動かす技術を、日本語で。

巨大バイナリ

2025年12月29日原文(fzakaria.com)

概要

  • 巨大バイナリ問題 は、特大規模のコードベースでのみ顕在化する現象
  • 2GiB「リロケーションバリア」 がx86_64アーキテクチャの制約
  • 静的リンク によりバイナリサイズが急増し、ジャンプ命令の制限に直面
  • -mcmodel=large で制限回避可能だが、命令肥大化などの副作用あり
  • さらなる解決策 は今後の議論対象

特大規模コードベースにおける巨大バイナリ問題

  • PhD研究や論文投稿 時に、超大規模な問題への理解不足を指摘される課題
  • Google等のメガコードベース でのみ発生する現象の観測
  • 25GiB超のELFバイナリ (デバッグシンボル含む)の実例
  • 静的リンク による全コードの取り込みでバイナリ肥大化
  • デプロイや起動高速化 のための静的ビルド選択

2GiB「リロケーションバリア」の正体

  • x86_64のCALL命令 は32bit符号付き相対オフセットのみ許容
  • 2^31(約2GiB) がジャンプ可能な最大範囲
  • 巨大バイナリ では関数間距離が2GiBを超えるケースが発生
  • 相対ジャンプの限界 が「リロケーションバリア」と呼ばれる理由

実例:シンプルな相対ジャンプとリロケーション

  • C言語の関数呼び出し をgccでコンパイルし、objdumpで解析
  • e8(CALL命令) が4バイトの相対位置を指定
  • readelfによるリロケーション情報 の確認
  • リンカスクリプト で関数を2GiB以上離すことで「relocation overflow」発生
  • エラーメッセージ 例: relocation R_X86_64_PLT32 out of range

回避策:-mcmodel=largeの利用

  • -mcmodel=large で64bit絶対アドレスジャンプに切り替え可能
  • 命令列が5バイト→12バイト に増加(MOVABS+CALL)
  • 汎用レジスタ(例: %rdx) の消費増加
  • 命令肥大化 によるバイナリサイズ増加・パフォーマンス影響
  • -fno-asynchronous-unwind-tables で追加データによるオーバーフロー防止

mcmodel=largeのデメリット

  • 命令バイト数増加 によるバイナリ肥大化
  • レジスタ消費 によるレジスタプレッシャー増
  • 理論上IPC低下 の可能性(実ベンチマークでは再現困難)

今後の展望と課題

  • 小規模コードモデル を維持しつつ制約を回避する新戦略の必要性
  • さらなる解決策や工夫 は今後の執筆で検討予定

Hackerたちの意見

25 GiBのバイナリって、ちょっと怖すぎるよね。動的リンクが必要なんじゃないかな。

確かに、これはデバッグシンボル込みの話だよね。数年前のChromeのデバッグビルドは5GBくらいだったし、今はもっと増えてるだろうね。オブジェクトファイルのサイズが大きすぎて、リンク中にラップトップがRAM不足になったこともあったな。デバッグシンボルってなんでこんなに大きいんだろう?C++の場合、プログラム内のあらゆる型のインスタンスに対して詳細な型情報が含まれてるから、フィールドの型(再帰的に)、メソッドのシグネチャ、ローカル変数の型と位置など、すごい量のデータが生成されるんだよね。これって「普通」なプロジェクトでもかなりのデータ量になる。さらに悪いことに、C++では動的リンクのメリットがあまりないんだ。ヘッダーファイルで定義されたテンプレートは簡単に共有ライブラリに入れられないし、ABIの違いで動的ライブラリは同期して更新しなきゃいけないし、モジュール間の重複も避けられない(インライン関数やテンプレートのおかげで)。古いバージョンの.soが「詰まる」と、デプロイが完全に壊れちゃうこともあるから、単一のバイナリをデプロイするよりもずっと厄介だよね(新しいバージョンか古いバージョンしか手に入らない、壊れたサービスはダメ)。

確かに、彼らはGoogleで働いてたから、エンジニアリングの判断が普通じゃないんだよね。25 GiBのバイナリがスタート時の0.25%のスピードアップに価値があると判断するかもしれないし、それが数千万ドルの差につながることもある。誰もGoogleのやり方を真似するべきじゃないけど、考えるのは面白いよね。

ダイナミックリンクだからって全体のサイズが小さくなるわけじゃないよ。逆に言うと、DLLはデッドコード削除の障壁になるからね。25GBはヤバすぎるし、開発の初期段階で何かがひどく間違ったに違いないよ(デバッグ情報を含めて出荷するのも、そもそも意味がわからないし)。

どうせ全部が一種のコンテナ(Dockerじゃないけど)に入ってるから、全然変わらないよ。もし、アプリが動く可能性のあるすべてのボーグマシンにそのライブラリをベースイメージとして配布することを提案してるなら、それは明らかに無理だね。

「25GiBを超えるバイナリを見たことがあるけど、デバッグシンボル込みでどうして可能なの?」 こういう会社は、スタティックビルドでサービスを構築して、起動を早くしたりデプロイを簡単にしたりするのが好きなんだよね。世界最大のコードベースのいくつかで全てのコードをスタティックに含めるのは、巨大なバイナリを生むレシピだよ。いいスタティックバイナリを一つのアーティファクトとして配布したい気持ちはすごく分かるけど、果たしてそれが本当に価値があるのかって考えなきゃいけないよね。実行可能なコードが2GBにも収まらないなら、それは本当に一つのバイナリのコードなのか、それとも分けるべきアプリが何個かあるのか、考えた方がいいんじゃない?それとも、たまには送るアーティファクトがtarballやOCIイメージ、systemd用のEROFSイメージ、自解凍アーカイブなんだって受け入れるべきかもね。 [0] 実際、今やってるプロジェクトの一つは、太ったELFバイナリを作るのがそんなに難しいのかを調べてるんだ。 [1] https://systemd.io/PORTABLE_SERVICES/ [2] https://justine.lol/ape.html > 「PKZIPの実行ファイルは結構いいコンテナになる」

これ、Googleで働いてた時もずっと気になってたことなんだ。すごい計算とストレージのインフラがあって、年々クレイジーになっていったけど(パフォーマンス、スケーラビリティ、冗長性の面で)、バイナリのサイズが大きすぎて、運用が遅く感じてた。コマンドラインのバイナリを実行するのも遅いし、デプロイ用のバイナリをビルドするのも遅い。バイナリをデプロイするのも遅い。バイナリのサイズが増えるたびに「インフラをスケールアップしよう!」って答えが返ってくるだけで、「こんなクレイジーなことはやめよう」って考えにはならなかった。私が辞める頃には、後者に向けた新しい取り組みが始まって、「もっと早く制限を設けるべきだったかも」って感じがあったけど、既存の膨張に制限を後付けするのはすごく難しいことだった。

もし25GBの実行ファイルがあるなら、それが一つのバイナリ実行ファイルか100個かは関係ないと思う。何かがひどくひどく間違ってる。4GBのバイナリを見たことはないけど、PDBファイルが4GBに達して問題が起きたことはある。デバッグシンボルがそんなに大きくなるのは全然あり得ることだね。それについてはまあ、いいかな。

https://systemd.io/PORTABLE_SERVICES/ Systemdとポータブル?

俺にとって驚きなのは、-gsplit-dwarfを使わないでデバッグ情報と「普通サイズ」のバイナリを分けないことだね。

ただ、最も簡単な解決策は -mcmodel=large を使うことで、全ての相対CALL命令を絶対JMPに変えることだよね。理にかなってるけど、その後のアセンブリ出力にはJMP命令が一つもない。代わりに、CALLは64ビットレジスタにアドレスを入れてからCALLするように置き換えられてる。これも理にかなってるけど、じゃあなんでJMPのことを言うの?間違いなのか、それとも何か見落としてるのかな?(いくつかのCALLはJMPに置き換えられるのは知ってるけど、それは -mcmodel=large に関係なく行われることだよね)

CALLをJMPと呼ぶような緩い言語だと思うけど、大きなコードモデルを嫌う理由の2つのうち、レジスタプレッシャーはその特定のスニペットには関係ないよ。コールを実行してるから、ABIはコール間で保持されないレジスタを定義してるし、そのうちの1つに宛先を書き込んでもレジスタプレッシャーには影響しないよ。

Hacker Newsで議論の続きを見る