CTC 教育サービス
[IT研修]注目キーワード Python Power Platform 最新技術動向 生成AI Docker Kubernetes
前回に続いて、2025年に公開された論文「Necro-reaper: Pruning away Dead Memory Traffic in Warehouse-Scale Computers」に基づいて、Googleの研究者が考案した、CPUのキャッシュメモリの処理効率を上げる仕組み「Necro-reaper」を解説していきます。今回は、プロファイルデータを用いた、コンパイラによる最適化処理を説明します。
前回の記事では、Necro-reaperは、次の2種類のCPUインストラクションを追加・利用することで、メインメモリとキャッシュメモリの間の無駄なデータ転送を削減することを説明しました。
(1) Cache Line Installation:メインメモリの特定のアドレス範囲をキャッシュメモリに割り当てるが、メインメモリのデータはキャッシュメモリには転送しない。
(2) Cache Line Invalidation:メインメモリのアドレス範囲に対するキャッシュメモリの割り当てを解放するが、キャッシュメモリのデータはメインメモリに書き戻さない。
また、これらのインストラクションを適切なタイミングで使用するために、専用のコンパイラを使用しますが、この際、コンパイル対象のプログラムのプロファイリングデータを用いて、さらなる最適化を実施します。(1)(2)の実行における最適化について、順に解説していきます。
まず、(1)のインストラクションは、ヒープメモリに新規メモリを割り当てる際に利用しますが、確保したメインメモリのすべてにキャッシュメモリを割り当てるのではなく、実際に書き込まれるデータ量の予測に応じて割り当てるキャッシュメモリの量を設定します。例えば、プログラムコードのある部分で、32KBのヒープメモリを確保したとします。プロファイラーを用いて、その後のプログラムの動作を追跡したところ、実際には、50%にあたる16KBしか使用しなかった場合、割り当てるキャッシュメモリは16KBで十分ということになります。そこで、Necro-reaperのコンパイラは、次の流れでコンパイル処理を行います。
図1の例では、オリジナルコード(Original Code)の1行目、6行目、10行目にヒープメモリを確保する命令(aligned_alloc)があります。これから生成されるプロファイル収集用のコード(Instrumented Code)では、それぞれがプロファイル収集用の命令(profile_alloc)に置き換えられており、これを実行すると、p1、p2、p3のそれぞれに対して、この後、実際に使用されるメモリ量が個別にプロファイルデータとして保存されます。

図1 プロファイルデータ収集用コードの生成例(論文より抜粋)
そして、得られたプロファイルデータは、p1が6%、p3が0%(割り当てたメモリを実際には使用しない)で、p2については、該当部分のコードが実行されず、プロファイルデータが得られなかったとします。これらのデータを元にして、最適化したコードは、図2のようになります。

図2 プロファイルデータに基づいて最適化されたコード(論文より抜粋)
p1とp3については、1行目と10行目で、プロファイルデータに基づいた割合のキャッシュメモリを確保します。また、プロファイルデータが無いp2については、6行目で、デフォルト値として、75%のキャッシュメモリを確保しています。もちろん、実際に利用されるメモリ量は実行ごとに変動する可能性があります。確保したキャッシュメモリが不足した場合は、CPUの通常の動作として、追加のキャッシュメモリが動的に割り当てられます。
続いて、(2)のインストラクションでキャッシュメモリを解放する部分の最適化を説明します。まず、Googleでは、C言語でのメモリ割り当て処理には、「第186回 Google独自開発のメモリ割り当てライブラリTCMalloc(パート1)」からの一連の記事で解説した、TCMallocと呼ばれる独自のライブラリを使用しています。TCMallocでは、「メモリオブジェクト」の単位でヒープメモリの割り当てを行いますが、メモリオブジェクトのサイズは、事前に定義された複数のサイズクラスに分かれており、アプリケーションがメモリ割り当てを要求すると、要求量を満たす最小サイズのメモリオブジェクトが割り当てられます。
ここで、次の様なシナリオを考えます。まず、プログラムコードのある部分で、64KBのメモリオブジェクトがヒープメモリから解放された後、また別の部分で、同じ64KBのメモリオブジェクトがヒープメモリに割り当てられたとします。この際、最初にメモリを解放する際に、あえて、該当部分のキャッシュメモリの割り当てを解放せずに残しておけば、次に割り当てる際は、既存のキャッシュメモリが再利用できます。つまり、メモリ開放時の(1)のインストラクションの発行、そして、再割り当て時の(2)のインストラクションの発行が省略できます。ただし、この場合、解放から再割り当てまでの間、該当のキャッシュメモリは利用されず、キャッシュメモリの利用効率が下がります。したがって、特定サイズのメモリオブジェクトについて、解放から再割り当てまでの期間が短い場合に限って、この最適化処理は有効性を発揮します。
そこで、Necro-reaperでは、メモリオブジェクトのサイズごとに、解放から再割り当てまでの「期間」を計測して、これをプロファイルデータとして保存します。先ほど「キャッシュメモリ割り当ての最適化」で説明したプロファイルデータは、プログラムコードの特定の行ごとにプロファイルデータを収集しましたが、こちらは、プログラムコードのどこで実行されるかには関係なく、単純にメモリオブジェクトのサイズごとにプロファイルデータを収集します。そして、収集した「期間」が一定値よりも短い場合に、キャッシュメモリを解放しない前述の最適化処理を実行します。先の図3のコードでは、free_invalidate関数内で実行している、skip_invalidate_size_class関数によって、sizeで指定されたサイズのメモリオブジェクトが最適化の対象かどうかをチェックしています。
なお、解放から再割り当てまでの「期間」については、実際の実行時間ではなく、解放から再割り当てまでの間に行われた(該当サイズ以外のメモリオブジェクトによる)ヒープメモリの割り当て量を計測します。この割り当て量の90パーセンタイルが1MB以下の場合に、前述の最適化を行います。実際の実行時間が長くても、その間にメモリ割り当て処理がなければ、確保したままのキャッシュメモリが無駄になるとは言えないので、このような実装を採用しています。
今回は、2025年に公開された論文「Necro-reaper: Pruning away Dead Memory Traffic in Warehouse-Scale Computers」に基づいて、CPUのキャッシュメモリの処理効率を上げる仕組み「Necro-reaper」について、プロファイルデータを用いたコンパイラによる最適化処理を説明しました。次回は、「Necro-reaper」の有効性を示す評価結果を紹介します。
Disclaimer:この記事は個人的なものです。ここで述べられていることは私の個人的な意見に基づくものであり、私の雇用者には関係はありません。
[IT研修]注目キーワード Python Power Platform 最新技術動向 生成AI Docker Kubernetes