3 月上旬に M89 リリースのロールアウトが開始された時点で、Windows のブラウザ プロセスのメモリ使用量を詳しく調査したものを示します。
Chrome は、マルチプラットフォーム、マルチプロセス、マルチスレッドのアプリケーションで、Android の小さな埋め込み WebView から宇宙船まで、実に幅広いニーズに対応しています。パフォーマンスとメモリのフットプリントは特に重要で、Chrome とメモリ アロケータには密接な統合が求められます。しかし、それぞれのプラットフォームには Linux と Chrome OS の tcmalloc、Android の jemalloc や scudo、Windows の LFH などの異なる実装があり、プラットフォームの違いを超えるのは難しい可能性もあります。
このプロジェクトに着手したときの目標は、1)プラットフォーム間でメモリ割り当てを統一すること、2)セキュリティやパフォーマンスを損なうことなく最小メモリ フットプリントを実現すること、3)Chrome のパフォーマンスの最適化にふさわしいアロケータを実現することでした。そこで、Chromium のクロスプラットフォームなアロケータを使う決定をしました。これは、サーバーのワークロードではなくクライアントのメモリ使用量を最適化するため、そして実際の使用例を意識しないマイクロベンチマークではなく有意義なエンドユーザーの活動に注目するためです。
アロケータのセキュリティ
PartitionAlloc は、独立した複数のパーティション(重複しないメモリ領域)をサポートするように設計されました。Blink では、文字列とレイアウト オブジェクトを確実に分離するなど、一部の形態の型混同攻撃を阻むために、全体にわたってこのパーティションを活用しています。しかし、このアプローチでは、別のパーティションで割り当てられた型同士の衝突しか避けることはできません。さらに、衝突する可能性があるオブジェクトのサイズが異なる場合、型の混同を避けるため、PartitionAlloc バケットはサイズを使って割り当てをします。この手法が動作するのは、PartitionAlloc がアドレス空間を再利用しないからです。PartitionAlloc がアドレス空間のある領域を特定のパーティションとサイズのバケットに割り当てる場合、その領域は常にそのパーティションとサイズのバケットに所属することになります。
さらに、PartitionAlloc は、メモリ領域周辺のガードページ(アクセスできない範囲)によって一部のメタデータを保護します。しかし、すべてのメタデータが同じとは限りません。以前に割り当てられた領域内には、フリーリストのエントリが格納されるので、他の割り当てに囲まれることになります。破損したフリーリストのエントリと off-by-one オーバーフローをクライアントのコードから検知するため、これをコード化して隠蔽します。
さらに、独自のアロケータが MiraclePtr や *Scan などの高度なセキュリティ機能を実現します。
アーキテクチャの詳細
PartitionAlloc の各パーティションは、メモリを節約するため、1 つの集中管理型のスラブベース アロケータを使用します。また、フロントでのスレッド単位のキャッシュは最低限にとどめ、マルチスレッドなワークロードにスケーリングできるようにしています。このシンプルな処理には、パフォーマンス面でのメリットもあります。Google は幅広いプロファイリングをし、アロケータの高速パスを徹底的に切り詰めました。これにより、スレッドローカルなストレージへのアクセスやロックが改善し、キャッシュ ラインの取得数は減少し、ブランチも削除できるようになっています。
PartitionAlloc は、仮想アドレス空間であらかじめスラブを予約します。割り当てリクエストが到着するにつれて、そこに物理メモリが徐々に割り当てられていきます。少量または中程度の割り当ては、[241; 256]、[257; 288] など、幾何学的に間隔を空けたサイズごとのバケットにグループ化されます。各スラブは、1 つの特定のバケットからのみ配分され、割り当て(「スロット」と呼ばれます)を満たす複数の領域(「スロットスパン」)に分割されます。そのため、キャッシュのローカル性は向上し、断片化は起こりにくくなります。逆に、大量の割り当てはバケットのロジックを通さず、直接オペレーティング システムのプリミティブ(POSIX システムでは mmap()、Windows では VirtualAlloc())を利用して実現します。
この集中管理型アロケータは、パーティション単位の 1 つのロックによって保護されます。競合によるスケーラビリティの問題を緩和するため、スレッド単位の小さなスロットのキャッシュをフロントに追加し、3 層型アーキテクチャを実現しています。
最初のレイヤー(スレッド単位のキャッシュ)は、頻繁に利用される小さなバケットに属する少量のスロットを保持します。これらのスロットはスレッドごとに保存されるため、ロックなしに割り当てることができ、必要になるのは高速なスレッドローカル ストレージの検索のみです。そのため、プロセスでのキャッシュのローカル性が向上します。このスレッドごとのキャッシュは、2 つ目のレイヤーのメモリをまとめて割り当てと解放をすることで、大半のリクエストを満たせるように最適化されています。そのため、過度なメモリを確保することなく、ロックの取得頻度を下げ、ローカル性をさらに向上することができます。
この 2 つ目のレイヤー(スロットスパンのフリーリスト)は、スレッドごとのキャッシュでキャッシュミスが発生した場合に呼び出されます。PartitionAlloc は、それぞれのバケットのサイズについて、そのサイズに関連付けられた空きスロットがあるスロットスパンを把握しています。そのため、そのスパンのフリーリストからスロットを取得します。この処理もまだ高速パス上にありますが、ロックの取得が必要なので、スレッドごとのキャッシュよりは遅くなります。しかし、このセクションにアクセスされるのは、スレッドごとのキャッシュでは対応できない大きな割り当てがされる場合や、スレッドごとのキャッシュを埋めるバッチとして実行される場合のみです。
最後に、バケットに空きスロットがない場合は、3 つ目のレイヤー(スロットスパン管理)が新しいスロットスパン用にスラブから領域を切り出すか、オペレーティング システムからまったく新しいスラブを割り当てます。これは遅い処理ですが、まれにしか起こらないオペレーションです。
このアロケータの全体的なパフォーマンスと領域の効率性は、キャッシュの量、バケットの数、メモリ再利用ポリシーなど、レイヤー間のさまざまなトレードオフ次第です。設計の詳細については、PartitionAlloc をご覧ください。
全体として、PartitionAlloc が実現するさらなるメモリ節約とパフォーマンスの向上によって、安全、軽量、高速な Chrome が実現し、それを地球上や宇宙空間のユーザーに利用していただけることを期待しています。今後の改善や、近いうちにされるその他のプラットフォームのサポートにもご期待ください。
すべての統計情報の出典 : Chrome クライアントから匿名で集計した実データ。
* 中心となる指標として、30 秒ごとにジャンク(ユーザーの入力を処理する際の遅延)を測定。