概要
キャッシュ(Cache)は、CPUと主記憶(DRAM等)の速度差を埋めるために設けられた、高速なSRAMによる一時記憶領域です。CPUが頻繁にアクセスするデータ・命令を高速なSRAMキャッシュに保持することで、低速なDRAMへのアクセス待ち時間を削減し、システム全体のスループットを向上させます。
現代のプロセッサはDRAMの100倍以上の速度で動作します(CPUコア: 〜1ns、DRAM: 〜50〜100ns)。キャッシュがないとCPUはDRAMのアクセスを待って大部分の時間をアイドル状態で過ごすことになります。キャッシュにより、データが高確率で近傍のSRAMで見つかり(キャッシュヒット)、CPUの実効スループットが大幅に向上します。
組み込みシステムでは:
- ARM Cortex-M7: L1命令キャッシュ16KB + データキャッシュ16KB(MPU対応)
- ARM Cortex-A: L1キャッシュ(16〜64KB)+ L2キャッシュ(256KB〜数MB)+ オプションでL3
- ARM Cortex-M0/M3/M4: キャッシュなし(フラッシュのウェイトステート設定で対応)
歴史・背景
キャッシュメモリの概念は1960年代に登場しました。IBMのSystem/360モデル85(1968年)が商用マシン初のキャッシュメモリを搭載しました。
1970〜80年代のミニコンピュータ・ワークステーションがキャッシュを採用し、CPUとメモリの速度差拡大とともにキャッシュの重要性が増しました。1980年代後半に外部キャッシュ(L2キャッシュ)が普及し、1990年代のPentium以降はL1・L2キャッシュがCPUに内蔵されるようになりました。
組み込みプロセッサでは、ARM Cortex-A8(2005年)が16KB L1キャッシュ+256KB L2キャッシュを搭載したことで、組み込みLinux向けの高性能MPUにキャッシュが標準化しました。ARM Cortex-M7(2014年)は高性能マイコン向けにL1キャッシュを搭載した初のCortex-Mコアです。
現代の組み込みSoC(NVIDIA Jetson・Raspberry Pi等)は数MB以上のL2/L3キャッシュを持ち、AI推論・画像処理・マルチタスク処理のパフォーマンスに大きく貢献しています。
技術仕様
キャッシュ階層
| キャッシュ | 典型サイズ | アクセス時間 | 実装 |
|---|---|---|---|
| L1 命令キャッシュ (I$) | 16〜64KB | 1〜4サイクル | コアに密結合(SRAM) |
| L1 データキャッシュ (D$) | 16〜64KB | 1〜4サイクル | コアに密結合(SRAM) |
| L2 キャッシュ | 256KB〜4MB | 10〜20サイクル | コア近傍(SRAM) |
| L3 キャッシュ | 4MB〜64MB | 30〜50サイクル | チップ上(SRAM、複数コアで共有) |
| 主記憶(DRAM) | GB単位 | 100〜300サイクル | オフチップDRAM |
キャッシュライン
キャッシュはキャッシュライン(Cache Line)単位でデータを管理します:
- ARM Cortex-M7: 32バイト/ライン
- ARM Cortex-A53/A57: 64バイト/ライン
- Intel Core: 64バイト/ライン
1バイトのデータにアクセスしても、そのアドレスを含むキャッシュライン全体(例: 64バイト)がDRAMからキャッシュに転送されます(空間的局所性の活用)。
キャッシュの書き込みポリシー
ライトスルー(Write-Through): CPUが書き込むとき、キャッシュとDRAMの両方に同時に書き込む。キャッシュとメモリが常に一致するが、書き込みが遅い。
ライトバック(Write-Back): CPUが書き込むとき、まずキャッシュのみに書き込む(ダーティフラグを立てる)。DRAMへの書き込みはキャッシュライン追い出し時(Eviction)に行う。高速だがキャッシュとメモリの不一致が生じる。
キャッシュの割り当てポリシー
ライトアロケート(Write-Allocate): 書き込みミス時にキャッシュラインをアロケートして書き込む。ライトバックと組み合わせて使われることが多い。
ノーライトアロケート(No-Write-Allocate): 書き込みミス時はキャッシュを使わずDRAMに直接書き込む。ライトスルーと組み合わせることが多い。
動作原理
キャッシュヒット・ミス
CPUがアドレスXのデータにアクセス
│
▼
キャッシュ確認
│
┌───┴───┐
│ │
ヒット ミス(キャッシュにない)
│ │
1〜4 DRAMから64Bのキャッシュラインをフェッチ
サイクル (100〜300サイクル)
で返す → キャッシュに格納
→ データを返す
セット連想キャッシュ(Set-Associative Cache)
現代のキャッシュはセット連想型です。キャッシュを複数のセット(Set)に分割し、各セットに複数のウェイ(Way)を持ちます。アドレスのインデックスビットでセットを選び、タグビットで一致するウェイを探します。
N-Way Set Associative Cache の構造例(4-way, 256セット):
アドレス = [タグ(上位ビット)][インデックス(8bit)][オフセット(6bit)]
インデックスで256セットのうち1つを選択
→ 4つのウェイのタグと比較
→ 一致すれば HIT、なければ MISS
コヒーレンシ(Cache Coherency)
マルチコアシステムでは複数のコアが同じメモリを異なるキャッシュにコピーして持つ可能性があり、データの一貫性(コヒーレンシ)が問題になります。ARMはMESI等のコヒーレンシプロトコルで各コアのキャッシュを管理し、一貫性を保証します。
DMAとキャッシュの不整合
DMAがDRAMに書き込んでも、CPUのキャッシュには古いデータが残っている(ステール)ことがあります。DMAでDRAMに書き込んだ後はキャッシュを無効化(Invalidate)し、CPUが書き込む前はキャッシュをフラッシュ(Flush/Clean)する必要があります。
// ARM Cortex-M7 DMAバッファのキャッシュ管理例
uint8_t dma_rx_buf[1024] __attribute__((aligned(32)));
// DMA受信開始前: DMAが書き込む領域のキャッシュを無効化
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, sizeof(dma_rx_buf));
HAL_UART_Receive_DMA(&huart1, dma_rx_buf, sizeof(dma_rx_buf));
// DMA完了コールバック: キャッシュ無効化してからデータを読む
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, sizeof(dma_rx_buf));
process_data(dma_rx_buf, sizeof(dma_rx_buf));
}
// DMA送信前: CPUが書いたデータをDRAMに書き戻す(Clean)
uint8_t dma_tx_buf[1024] __attribute__((aligned(32)));
prepare_tx_data(dma_tx_buf);
SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buf, sizeof(dma_tx_buf));
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, sizeof(dma_tx_buf));
用途・ユースケース
ARM Cortex-M7のキャッシュ有効化
STM32H7等のCortex-M7ベースマイコンでキャッシュを有効化するとコードの実行速度が大幅に向上します:
// キャッシュ有効化(システム起動時に実施)
SCB_EnableICache(); // 命令キャッシュ有効化
SCB_EnableDCache(); // データキャッシュ有効化
// MPU設定でキャッシュポリシーを制御
MPU_Region_InitTypeDef mpu_init;
mpu_init.Enable = MPU_REGION_ENABLE;
mpu_init.BaseAddress = 0x24000000; // SRAM1
mpu_init.Size = MPU_REGION_SIZE_512KB;
mpu_init.AccessPermission = MPU_REGION_FULL_ACCESS;
mpu_init.IsBufferable = 1; // ライトバック
mpu_init.IsCacheable = 1; // キャッシュ有効
mpu_init.IsShareable = 0;
mpu_init.Number = MPU_REGION_NUMBER0;
HAL_MPU_ConfigRegion(&mpu_init);
ARM Cortex-Aでのキャッシュ管理
組み込みLinuxでは通常カーネルがキャッシュを自動管理します。カーネルドライバでDMAを使う場合、dma_map_single()・dma_unmap_single()がキャッシュのフラッシュ・無効化を自動処理します:
// Linuxカーネルドライバ DMA API(キャッシュを自動管理)
dma_addr_t dma_handle;
void *buf = kmalloc(BUF_SIZE, GFP_KERNEL);
dma_handle = dma_map_single(dev, buf, BUF_SIZE, DMA_FROM_DEVICE);
// DMA操作...
dma_unmap_single(dev, dma_handle, BUF_SIZE, DMA_FROM_DEVICE);
// この時点でキャッシュが自動的に無効化されcpuから読める
キャッシュ効果の計測
// キャッシュの効果確認(サイクルカウンタで計測)
DWT->CYCCNT = 0; // サイクルカウンタリセット
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
access_array_sequential(data, N); // シーケンシャルアクセス(キャッシュ効果大)
uint32_t cycles_seq = DWT->CYCCNT;
DWT->CYCCNT = 0;
access_array_random(data, N); // ランダムアクセス(キャッシュ効果小)
uint32_t cycles_rnd = DWT->CYCCNT;
// シーケンシャル >> ランダム(キャッシュヒット率の差)
実装・開発のポイント
キャッシュフレンドリーなデータ構造
// キャッシュ非効率: AoS(Array of Structures)でランダムフィールドアクセス
typedef struct { float x, y, z, w; } Vec4;
Vec4 particles[10000];
// x成分だけ更新: x・y・z・wが同一キャッシュラインに入るが y・z・w は無駄
for (int i = 0; i < 10000; i++) particles[i].x *= 2.0f;
// キャッシュ効率的: SoA(Structure of Arrays)
struct Particles {
float x[10000], y[10000], z[10000], w[10000];
};
// x成分だけ更新: x配列のみアクセス → 高いキャッシュヒット率
for (int i = 0; i < 10000; i++) particles.x[i] *= 2.0f;
ノンキャッシャブル領域の設定
MPUやMMUを使って、DMAバッファ・ペリフェラルレジスタ・共有メモリをキャッシュ不可(Non-Cacheable)に設定することで、キャッシュコヒーレンシの問題を根本的に回避できます。ただし、この領域へのアクセスは低速になります。
// MPUでDMAバッファ領域をノンキャッシャブルに設定
mpu_init.IsBufferable = 0;
mpu_init.IsCacheable = 0; // キャッシュ無効
mpu_init.BaseAddress = (uint32_t)dma_buffer_region;
HAL_MPU_ConfigRegion(&mpu_init);
他技術との比較
キャッシュ vs TCM(Tightly Coupled Memory)
TCM(SRAMをコアに直結)はキャッシュと異なり、プログラマが明示的に配置を制御できます。キャッシュはハードウェアが自動管理(アクセスパターン依存)しますが、TCMは確定的なアクセス時間を保証できるため、リアルタイム性が求められるISR・制御ループに適しています。
キャッシュ vs プリフェッチ
プリフェッチ(Prefetch)はキャッシュとは別に、CPUが近い将来必要になりそうなデータを予めDRAMから読み込む技術です。ARM Cortex-Aのハードウェアプリフェッチャが自動的にシーケンシャルアクセスパターンを検出し、次のキャッシュラインを先読みします。キャッシュとプリフェッチを組み合わせることで、メモリ帯域幅を最大限に活用できます。