CPU使用率が高いのは、データベースがメモリ待ちになっているサインかもしれない
データベースの集計処理において、メモリレイテンシがCPUをどのように待たせるのか、そして基数分割(radix partitioning)によるキャッシュ効率化がパフォーマンスをどう向上させるかについて詳しく解説します。
英語の原文から自動翻訳されました。
データベースサーバーが100%のCPU使用率を示していても、必ずしも計算能力をフルに活用しているわけではありません。多くの場合、プロセッサはメインメモリからのデータ到着を待ってアイドル状態になっています。Pivotチームによる最近の技術分析では、一般的なデータベース操作における主要なボトルネックは計算複雑性ではなく、メモリレイテンシであることが明らかになりました。
何が起きたのか
Pivotエンジニアリングチームは、集計クエリ実行中にデータベースエンジンが完全に稼働しているように見えるにもかかわらず、期待より遅い理由を調査しました。彼らは9,900万行を含むClickBenchデータセットを使用し、標準的なGROUP BY操作に焦点を当てました。特定のクエリでは一意なUserIDの出現回数をカウントし、約1,700万個の異なるグループを作成しました。性能特性を分離するため、AMD EPYC 9124 Genoaプロセッサを搭載したベアメタルサーバーでテストを実行し、並列タスクからの干渉を避けるためにプロセスを単一コアに固定しました。
初期実装では単純なアプローチを採用しました。UserIDをキーとする単一の成長するHashTableです。各行について、エンジンはキーの存在を確認し、カウントを更新しました。この方法では初回の実行に3.9秒、OSのページキャッシュにデータが既にキャッシュされている subsequent runs で3.6秒かかりました。これらの時間は許容範囲に見えるかもしれませんが、内部のパフォーマンスカウンタは別の事実を示していました。CPUは時間の半分を、メモリアクセスの完了を待つだけで何もせずに費やしていたのです。
これを解決するために、エンジニアは単一のHashTableを2段階の基数分割アルゴリズムに置き換えました。第1段階では、各キーをハッシュ化し、ハッシュ値の上位ビットに基づいて行を4,096個の個別パーティションに分散させます。第2段階では、各パーティションを個別に処理し、それぞれに対して小さな一時HashTableを作成します。この変更により、キャッシュ済みデータでのクエリ時間は2.7秒に短縮されました。さらにPivotエンジン内の最適化により、最終的に単一コアでの時間を1.8秒まで低減することに成功し、データアクセスパターンの構造変更が大きな性能向上をもたらすことを証明しました。
仕組み
現代のCPUは、RAMからデータを取得する速度よりもはるかに高速に命令を実行します。プロセッサは1ナノ秒あたり4〜8命令を完了できる一方、DRAMへの単一アクセスには50〜100ナノ秒かかります。その待ち時間中に、CPUは数百命令を実行できた可能性があります。これを緩和するために、CPUはメインメモリより高速だが容量の小さい階層型キャッシュ(L1、L2、L3)を使用します。操作に必要なデータがキャッシュにない場合、CPUはストールします。
単純なGROUP BY実装では、数百万の一意なキーを収めるためにHashTableが成長します。テーブルが拡大すると、L1およびL2キャッシュの容量を超えます。結果として、ほぼすべてのルックアップが「キャッシュミス」となり、CPUは低速なDRAMからデータをフェッチせざるを得なくなります。パフォーマンス指標はこの事実を確認しました。CPUサイクルの50.1%がメモリバウンドによるストールで消費され、実際の作業に使用されたのはわずか16.1%でした。システムはクエリのためにDRAMから4.5GBのデータを取得しました。
基数分割は、問題をキャッシュに収まる小さなチャンクに分割することでこの課題を解決します。1,700万個のキーを4,096個のパーティションに分けることで、個々のパーティションのHashTableは処理中に高速なL1またはL2キャッシュ内に留まれるほど小さくなります。このアプローチは合計命令数が増加しますが(2回のハッシュ化と複数のベクトルの管理が必要)、低速なDRAMアクセスの数を劇的に削減します。最適化された実行では、メモリストールが23.5%に低下し、有用な作業が32.2%に上昇しました。DRAMトラフィックは1.5GBに減少し、追加のCPU命令と引き換えにキャッシュ局所性を改善することが有効な戦略であることを証明しました。
重要な詳細
- DRAMへのメモリアクセスはL1キャッシュアクセスより50〜100倍遅く、重大なCPUストールを引き起こします。
- 単純なHashTableアプローチでは、CPUサイクルの50.1%がメモリ待ちで無駄になっていました。
- 基数分割はデータを4,096個のチャンクに分割し、ワーキングセットがCPUキャッシュに収まるようにします。
- 最適化された分割アプローチにより、同じクエリでのDRAMデータ転送量が4.5GBから1.5GBに削減されました。
- 合計命令数は増加しましたが、キャッシュフレンドリーな手法によりクエリ時間は25%以上短縮されました。
- 最終的な最適化された単一コア性能は1.8秒に達し、16コア並列実行では171msを記録しました。
なぜ重要なのか
ソフトウェアエンジニアやデータベース開発者にとって、この分析はCPU使用率メトリクスが誤解を招く可能性があることを強調しています。100%のCPU使用率を示すサーバーは、実際には計算能力ではなくデータ不足に苦しんでいるかもしれません。コンピュートバウンドとメモリアバウンドのワークロードの違いを理解することは、効果的なパフォーマンスチューニングに不可欠です。キャッシュ局所性の最適化は、単にコアを追加したりクロック速度を上げたりするよりも、しばしばより大きな改善をもたらします。
この洞察は、頻繁なルックアップや集計を伴う大規模データセットを扱うあらゆるシステムに広く適用できます。高性能バックエンドを開発するエンジニアは、設計プロセスの早い段階でデータレイアウトとアクセスパターンを検討すべきです。紙面上では計算負荷が高く見えるアルゴリズムでも、メモリ階層の物理的制約を尊重すれば、実践的にはより良い性能を発揮する場合があります。これらのハードウェア現実を無視すると、豊富な計算資源があるにもかかわらずスケーラビリティの悪いシステムにつながります。
できること
- CPU時間だけでなく、メモリストールサイクルとキャッシュミス率を測定するために
perf statなどのツールを使用してください。 - データアクセスパターンを分析し、大きくランダムなメモリ領域に触れる操作を特定してください。
- L2またはL3キャッシュサイズに収まるよう、大規模データセットを小さなチャンクに分割することを検討してください。
- 理論的な時間計算量だけでなく、キャッシュ局所性に基づいてアルゴリズムを評価してください。
- ベンチマーキング中は、コンテキストスイッチによるノイズを減らすため、重要なプロセスを特定のCPUコアに固定してください。
- アプリケーション内のメモリアバウンドボトルネックの主要な指標として、DRAM帯域幅の使用状況を監視してください。



