CPUキャッシュを使用してコードを高速化する方法

CPUのキャッシュは、メインシステムメモリからデータにアクセスするときのメモリレイテンシを削減します。開発者は、CPUキャッシュを利用して、アプリケーションのパフォーマンスを向上させることができます。

CPUキャッシュのしくみ

最近のCPUには通常、L1、L2、およびL3というラベルの付いた3つのレベルのキャッシュがあり、CPUがそれらをチェックする順序を反映しています。多くの場合、CPUには、データキャッシュ、命令キャッシュ(コード用)、および統合キャッシュ(あらゆるもの用)があります。これらのキャッシュへのアクセスは、RAMへのアクセスよりもはるかに高速です。通常、L1キャッシュはデータアクセス用のRAMよりも約100倍高速であり、L2キャッシュはデータアクセス用のRAMよりも25倍高速です。

ソフトウェアが実行され、データまたは命令を取り込む必要がある場合、最初にCPUキャッシュがチェックされ、次に低速のシステムRAM、最後にはるかに低速のディスクドライブがチェックされます。そのため、コードを最適化して、最初にCPUキャッシュから必要になる可能性のあるものを探します。

コードでは、データ命令とデータが存在する場所を指定できません(コンピューターハードウェアが指定できます)。そのため、特定の要素をCPUキャッシュに強制することはできません。ただし、Windows Management Instrumentation(WMI)を使用して、コードを最適化してシステム内のL1、L2、またはL3キャッシュのサイズを取得し、アプリケーションがキャッシュにアクセスするタイミングを最適化して、そのパフォーマンスを最適化できます。

CPUがバイトごとにキャッシュにアクセスすることはありません。代わりに、キャッシュラインのメモリを読み取ります。キャッシュラインは、通常32、64、または128バイトのサイズのメモリのチャンクです。

次のコードリストは、システムでL2またはL3CPUキャッシュサイズを取得する方法を示しています。

public static uint GetCPUCacheSize(string cacheType){try {using(ManagementObject managementObject = new ManagementObject( "Win32_Processor.DeviceID = 'CPU0'")){return(uint)(managementObject [cacheType]); }} catch {return 0; }} static void Main(string [] args){uint L2CacheSize = GetCPUCacheSize( "L2CacheSize"); uint L3CacheSize = GetCPUCacheSize( "L3CacheSize"); Console.WriteLine( "L2CacheSize:" + L2CacheSize.ToString()); Console.WriteLine( "L3CacheSize:" + L3CacheSize.ToString()); Console.Read(); }

Microsoftには、Win32_ProcessorWMIクラスに関する追加のドキュメントがあります。

パフォーマンスのためのプログラミング:サンプルコード

スタックにオブジェクトがある場合、ガベージコレクションのオーバーヘッドはありません。ヒープベースのオブジェクトを使用している場合、ヒープ内のオブジェクトを収集または移動したり、ヒープメモリを圧縮したりするための世代別ガベージコレクションには常にコストがかかります。ガベージコレクションのオーバーヘッドを回避する良い方法は、クラスの代わりに構造体を使用することです。

キャッシュは、配列などのシーケンシャルデータ構造を使用している場合に最適に機能します。順次順序付けにより、CPUは先読みすることができ、次に要求される可能性が高いものを見越して投機的に先読みすることもできます。したがって、メモリに順次アクセスするアルゴリズムは常に高速です。

ランダムな順序でメモリにアクセスする場合、CPUはメモリにアクセスするたびに新しいキャッシュラインを必要とします。これにより、パフォーマンスが低下します。

次のコードスニペットは、クラスよりも構造体を使用する利点を示す簡単なプログラムを実装しています。

 struct RectangleStruct {public int width; public int height; } class RectangleClass {public int width; public int height; }

次のコードは、クラスの配列に対して構造体の配列を使用した場合のパフォーマンスをプロファイルします。説明のために、両方に100万個のオブジェクトを使用しましたが、通常、アプリケーションにはそれほど多くのオブジェクトは必要ありません。

static void Main(string [] args){const int size = 1000000; var structs = new RectangleStruct [size]; var classes = new RectangleClass [size]; var sw = new Stopwatch(); sw.Start(); for(var i = 0; i <size; ++ i){structs [i] = new RectangleStruct(); structs [i] .breadth = 0 structs [i] .height = 0; } var structTime = sw.ElapsedMilliseconds; sw.Reset(); sw.Start(); for(var i = 0; i <size; ++ i){classes [i] = new RectangleClass(); classes [i] .breadth = 0; classes [i] .height = 0; } var classTime = sw.ElapsedMilliseconds; sw.Stop(); Console.WriteLine( "クラスの配列にかかる時間:" + classTime.ToString()+ "ミリ秒。"); Console.WriteLine( "構造体の配列にかかる時間:" + structTime.ToString()+ "ミリ秒。"); Console.Read(); }

プログラムは単純です。100万個の構造体オブジェクトを作成し、それらを配列に格納します。また、クラスの100万個のオブジェクトを作成し、それらを別の配列に格納します。プロパティの幅と高さには、インスタンスごとにゼロの値が割り当てられます。

ご覧のとおり、キャッシュに適した構造体を使用すると、パフォーマンスが大幅に向上します。

CPUキャッシュの使用率を向上させるための経験則

では、CPUキャッシュを最適に使用するコードをどのように記述しますか?残念ながら、魔法の公式はありません。しかし、いくつかの経験則があります。

  • 不規則なメモリアクセスパターンを示すアルゴリズムやデータ構造の使用は避けてください。代わりに線形データ構造を使用してください。
  • より小さなデータ型を使用し、データを整理して、位置合わせの穴がないようにします。
  • アクセスパターンを検討し、線形データ構造を利用します。
  • 空間的局所性を改善します。これにより、キャッシュにマップされた後、各キャッシュラインが最大限に使用されます。