キャッシュキーを64バイトから32バイトへ縮小し、パディングによる不安定性を解消
Bootsnapのキャッシュファイルヘッダーが64バイトから32バイトに削減され、未初期化パディングバイトに起因するビルドの非再現性問題が根本から解消されました。キャッシュキーのフィールド設計も刷新され、より合理的な構造になっています。
背景
bootsnap precompile を複数回実行した際に、compile-cache-iseq および compile-cache-yaml 配下のキャッシュファイルが数バイト単位で異なるという問題が #542 で報告されました。ビルドイメージの再現性向上に取り組んでいたユーザーが、ファイル差分を調査する中でこの挙動を発見しました。
差分の原因は bs_cache_key 構造体の pad フィールド にありました。C言語の構造体はメモリ上でアライメントのためにパディングを挿入することがあり、その領域は明示的に初期化しない限りスタック上の任意の値を持ちます。Bootsnapはこの構造体をそのままキャッシュファイルの先頭に書き込んでいたため、パディング部分がコンパイル実行ごとに異なる値になっていました。
キャッシュの正確性には影響がないものの、コンテナイメージのレイヤーキャッシュやビルド成果物の比較など、バイト列の同一性を前提とするワークフローでは実質的な問題を引き起こしていました。
技術的な変更
bs_cache_key 構造体が全面的に再設計され、パディングフィールドそのものを除去することで問題を根本解決しています。
変更前の構造体(64バイト):
struct bs_cache_key {
uint32_t version; // 4バイト
uint32_t ruby_platform; // 4バイト
uint32_t compile_option; // 4バイト
uint32_t ruby_revision; // 4バイト
uint64_t size; // 8バイト
uint64_t mtime; // 8バイト
uint64_t data_size; // 8バイト
uint64_t digest; // 8バイト
uint8_t digest_set; // 1バイト
uint8_t pad[15]; // 15バイト(パディング)
} __attribute__((packed));
変更後の構造体(32バイト):
struct bs_cache_key {
uint64_t ruby_version_digest; // 8バイト
uint64_t mtime; // 8バイト
uint64_t digest; // 8バイト
uint32_t size; // 4バイト
uint32_t data_size; // 4バイト
} __attribute__((packed));
最も重要な変更は pad[15] フィールドの完全な削除です。同時に、version・ruby_platform・compile_option・ruby_revision の4つの uint32_t フィールドが ruby_version_digest という単一の uint64_t に統合されました。この新フィールドは RUBY_DESCRIPTION 定数の文字列(Rubyのバージョン、プラットフォーム、ビルドオプションを含む)とBootsnapのキャッシュバージョン番号、および RubyVM::InstructionSequence.compile_option の内容をまとめてハッシュ化した値です。
テストコードもこの構造変更に追従しており、各フィールドのバイトオフセットが更新されています。
R = {
ruby_version_digest: 0...8,
mtime: 8...16,
digest: 16...24,
size: 24...28,
data_size: 28...32,
}.freeze
CACHE_KEY_SIZE = 32
また、test/test_helper.rb では fnv1a_64 の実装が fnv1a_64_iter という初期ハッシュ値を受け取るヘルパーに分離され、複数フィールドのデータを連鎖してハッシュ化できるよう拡張されています。これは ruby_version_digest の生成ロジックを反映したものです。
設計判断
パディングをゼロ埋めする回避策ではなく、パディング不要な構造体への再設計 が選択されました。
#542 の報告者自身が memset によるゼロ埋めという回避策を提案していましたが、このPRではより根本的なアプローチが採られています。分散していた4つの環境識別フィールドを単一のダイジェスト値に集約したことで、パディングが発生しない自然な64ビットアライメントが実現し、構造体の全バイトが有意な値で埋まる設計になりました。
size フィールドが uint64_t から uint32_t に縮小されている点も注目されます。ソースファイルのサイズを表すフィールドとして、32ビット(最大約4GB)で実用上十分と判断され、data_size と合わせて uint32_t に揃えることで末尾の4バイトを節約しています。
まとめ
パディングの除去と構造体の再設計により、Bootsnapのキャッシュファイルはバイト単位で決定論的になりました。ヘッダーサイズの削減は付随的な効果ですが、パディングを受け入れる場所を作らない設計への移行が本質的な改善点です。コンテナイメージのビルド再現性を重視する環境では、この変更により bootsnap precompile の出力を安定したビルド成果物として扱えるようになります。