CPU 캐시 (CPU Cache)

CPU 캐시를 하드웨어 미시 구조와 커널 성능 튜닝 관점에서 함께 설명합니다. L1/L2/L3 계층과 TLB 상호작용, 인덱싱 방식(VIPT/PIPT)과 alias 이슈, 코히런시 프로토콜(MESI/MOESI/MESIF), 프리페처·교체 정책, Intel RDT(CAT/MBA), NUMA 캐시 affinity, false sharing 탐지와 perf c2c 기반 진단 절차까지 종합적으로 다룹니다.

CPU 캐시는 프로세서와 메인 메모리 사이의 속도 차이(수백 배)를 완화하기 위한 고속 SRAM 버퍼(Buffer)입니다. 리눅스 커널은 캐시 라인(Cache Line) 정렬, 코히런시, TLB 관리, 프리페칭 등 다양한 수준에서 캐시를 인식하고 활용합니다. 이 페이지(Page)에서는 하드웨어 캐시의 원리부터 커널의 캐시 API, 실전 진단까지 종합적으로 다룹니다.

관련 페이지: CPU 토폴로지(Topology)와 캐시 공유 관계는 CPU 토폴로지를, NUMA 노드별 메모리 배치는 NUMA를, DMA 캐시 동기화는 메모리를 참조하세요.
관련 표준: Intel SDM Vol.3 (캐시 제어, MESI 프로토콜), AMD APM (캐시 계층, MOESI) — CPU 캐시 아키텍처와 일관성 프로토콜의 핵심 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
커널 버전 기준: 이 문서의 API·구조체·파일 경로·버전별 기능 설명은 Linux 7.2Stable 기준입니다 (확인 시점: 2026-10-05, Stable 7.2.9 / Mainline 7.3-rc6). 각 기능의 최초 도입 버전은 본문에 Linux 6.x 형태로 함께 표기했습니다. 커널이 올라갈수록 소스 경로(fs/resctrl/, drivers/resctrl/ 등)는 바뀌므로, 실제 디버깅(Debugging) 시에는 /proc/version과 대응하는 태그의 소스를 함께 확인하세요.
전제 조건: CPU 토폴로지와 어셈블리(Assembly) 문서를 먼저 읽으세요. CPU 구조 주제는 하드웨어 계층과 명령어 수준 제어가 맞물리므로, 코어/캐시/레지스터(Register) 경계를 먼저 정리해야 합니다.
일상 비유: 이 개념은 책상 위 메모지와 비슷합니다. 자주 보는 자료를 손이 닿는 가까운 곳에 두듯이, CPU도 자주 쓰는 데이터를 캐시에 두어 메모리 접근 지연을 줄입니다.

핵심 요약

  • L1/L2/L3 — 캐시 계층. L1이 가장 빠르고 작으며(32–64KB), L3가 가장 크고 느립니다(수십 MB).
  • 캐시 라인 — 64바이트 단위로 데이터를 캐시에 적재합니다. 연속 메모리 접근이 빠른 이유입니다.
  • MESI 프로토콜 — 멀티코어 환경에서 캐시 일관성(coherency)을 유지하는 프로토콜입니다.
  • TLB — 페이지 테이블(Page Table) 변환 결과를 캐싱하는 특수 캐시로, 가상→물리 주소 변환(Address Translation)을 가속합니다.
  • False Sharing — 서로 다른 변수가 같은 캐시 라인에 있어 불필요한 동기화가 발생하는 성능 문제입니다.
  • VIPT/PIPT — ARM L1D는 VIPT(가상 인덱스/물리 태그) 방식으로, 컨텍스트 스위치 시 캐시를 전부 플러시(Flush)하지 않아도 됩니다.
  • NUMA 지역성 — 원격 NUMA 노드의 LLC 접근은 로컬 LLC보다 느립니다. 배율은 아키텍처·하드웨어에 따라 다르며 대체로 수 배 수준입니다. numactl --membind로 지역성을 강제하세요.
  • PMEM 영속성 — CLWB+SFENCE 없이는 전원 장애 시 캐시에 남아 있는 데이터가 소실될 수 있습니다. DAX 경로에서 반드시 필요합니다.

단계별 이해

  1. 구성 확인
    lscpu, getconf -a | grep CACHE, /sys/devices/system/cpu/cpu0/cache/로 계층별 크기·라인 크기·인덱싱 방식을 확인합니다.
  2. Hit/Miss 측정
    perf stat -e cache-misses,cache-references와 bpftrace로 프로세스별 캐시 효율·핫스팟을 관찰합니다.
  3. 코히런시와 False Sharing
    MESI 프로토콜의 무효화 동작을 이해하고 ____cacheline_aligned로 구조체를 정렬해 False Sharing을 줄입니다.
  4. NUMA 지역성
    numastat -c, perf stat -e LLC-load-misses,node-load-misses로 원격 LLC 비용을 파악하고 numactl/taskset으로 고정합니다.
  5. PMEM·심화
    커널 DAX 경로는 arch_wb_cache_pmem()(x86에서는 clwb 루프)이 영속화를 담당하고, 사용자 공간은 clwb + sfence 패턴 또는 PMDK pmem_persist()로 직접 보장합니다.

캐시 기본 원리

왜 캐시가 필요한가

CPU의 연산 속도는 수십 년간 꾸준히 향상됐지만, 메인 메모리(DRAM)의 접근 지연은 수십~수백 ns 수준에 머물고 있습니다. 3 GHz CPU 기준으로 1 클록 사이클은 약 0.33 ns이므로, DRAM에서 데이터를 한 번 가져오는 데 약 200~300 사이클이 소요됩니다. CPU가 이 대기 시간(Latency) 동안 아무것도 하지 못한다면 처리 성능은 DRAM 속도에 묶이게 됩니다. 이 간극을 메모리 벽(Memory Wall)이라 합니다.

해결책은 CPU 코어 가까이에 소용량·고속 SRAM 계층을 여러 단계로 배치하는 것입니다. 빠른 계층은 가까이, 느린 계층은 멀리 두고, 각 계층이 '위' 계층에서 자주 쓰이는 데이터를 보관합니다. 이것이 L1/L2/L3 캐시 계층입니다.

레지스터 Flip-Flop L1 캐시 SRAM · 코어별 독립 L2 캐시 SRAM · 코어별 독립 L3 / LLC SRAM · 소켓 내 공유 DRAM 메인 메모리 · 리프레시 필요 NVMe SSD 비휘발성 · 블록 기반 ≤ 1 사이클 4~5 사이클 12~14 사이클 30~50 사이클 ~200 사이클 (80 ns) ~100,000 사이클 수백 B 32~192 KB 256 KB~4 MB 수~수백 MB 수~수백 GB 수~수십 TB [ 캐시 = L1·L2·L3 계층 ]  ↑ 위로 갈수록 빠름·소용량·고비용  /  아래로 갈수록 느림·대용량·저비용 ↓
SRAM vs DRAM: 캐시(L1/L2/L3)는 SRAM(Static RAM)으로 만들어집니다. SRAM은 플립플롭(트랜지스터 6개/비트)으로 구성되어 리프레시 없이 고속 동작합니다. DRAM은 커패시터(트랜지스터 1개/비트)로 고밀도·저비용이지만 주기적 리프레시가 필요합니다. 동일 용량 기준 SRAM 제조 비용은 DRAM보다 수십 배 높아, L3 캐시가 수십 MB 이상을 넘기 어려운 이유가 됩니다.

캐시 라인

캐시의 최소 전송 단위는 캐시 라인(cache line)으로, 현대 x86/ARM 프로세서에서 대부분 64바이트입니다. 메모리 주소를 캐시 라인 크기로 나눈 몫이 같은 바이트들은 항상 함께 캐시에 올라옵니다. 따라서 구조체(Struct)의 핫 필드를 같은 캐시 라인에 배치하면 하나의 캐시 미스로 여러 필드를 동시에 읽을 수 있습니다.

/* include/linux/cache.h */
#ifndef L1_CACHE_BYTES
#define L1_CACHE_BYTES  (1 << L1_CACHE_SHIFT)  /* x86: 64 */
#endif

#define ____cacheline_aligned  __attribute__((__aligned__(L1_CACHE_BYTES)))

/* 핫 필드를 캐시 라인 경계에 정렬 */
struct net_device {
    char                    name[IFNAMSIZ];
    /* ... 핫 패스 필드 ... */
    unsigned long           state;
    /* 콜드 필드는 별도 캐시 라인으로 분리 */
    struct list_head        dev_list ____cacheline_aligned_in_smp;
};
64바이트 캐시 라인 로딩 개념 — 4바이트 요청과 64바이트 전송의 차이 64바이트 캐시 라인 단위의 이유 ① CPU 요청 = int 1개 (4바이트) 메모리 (DRAM) … 이전 캐시 라인 다음 캐시 라인 … a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9] a[10] a[11] a[12] a[13] a[14] a[15] ② 메모리가 옮기는 크기 = 캐시 라인 1개 = 64바이트 (int 16개) = 요청의 16배 요청하지 않은 나머지 60바이트가 한 번에 함께 딸려옵니다 ③ DRAM에서 64바이트 라인 전체를 1회 읽어 L1로 적재 L1 캐시 a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9] a[10] a[11] a[12] a[13] a[14] a[15] ④ 이후 a[1]~a[15] 접근 → 모두 L1 히트 (DRAM 추가 접근 없음) 결과: int 16개 접근 중 DRAM 접근은 1회 — 나머지 15회는 L1에서 처리 CPU 요청 지점 (캐시 미스) 같은 라인으로 함께 적재 (아직 미사용) L1 히트 (지금 사용)
CPU가 int 하나(4바이트)만 요청해도 캐시는 64바이트인 캐시 라인 단위로 옮기는 이유 — 요청 크기(4바이트)와 전송 크기(64바이트)의 차이가 이 그림의 핵심입니다. 64바이트·int 16개를 기준으로 한 개념 개요도이며, 실제 세트/웨이 분해는 뒤의 태그/셋/오프셋 비트 분해 다이어그램에서 다룹니다.

공간적 / 시간적 지역성

캐시가 효과적인 이유는 프로그램의 지역성(locality) 때문입니다.

지역성의 차이가 실제 성능에 미치는 영향을 2차원 행렬 순회로 확인할 수 있습니다:

/* 공간적 지역성 비교: 행(row) 우선 vs 열(column) 우선 접근 */
#define N 1024
int mat[N][N];   /* 4MB — L3 캐시 범위 밖 */
long sum = 0;

/* ① 행 우선 (Row-major) — 공간적 지역성 우수 */
for (int i = 0; i < N; i++)
    for (int j = 0; j < N; j++)
        sum += mat[i][j];
/*
 * mat[i][j], mat[i][j+1], … 는 메모리에 연속 배치(C 행 우선)
 * → 캐시 라인 1개 로드 시 int 16개가 한꺼번에 올라옴
 * → 16회 접근마다 캐시 미스 1회
 */

/* ② 열 우선 (Column-major) — 공간적 지역성 불량 */
for (int j = 0; j < N; j++)
    for (int i = 0; i < N; i++)
        sum += mat[i][j];
/*
 * mat[0][j], mat[1][j], … 는 4096바이트(N×sizeof(int)) 간격으로 점프
 * → 매 접근마다 새 캐시 라인을 로드
 * → 16회 접근마다 캐시 미스 16회 (히트율 0%)
 */
방식캐시 라인 패턴L1D 미스 특성상대 실행 시간
① 행 우선라인 1개 로드 → int 16개 히트낮음 — 캐시 라인 접근당 미스 1회1× (기준)
② 열 우선매 접근마다 새 캐시 라인 로드매우 높음 — 접근 1회당 미스 1회크게 증가
정량 수치는 직접 측정하세요: 미스율과 상대 실행 시간은 CPU 모델·클럭·L2/L3 용량·컴파일러의 최적화 수준에 따라 크게 달라집니다. 위 두 방식의 실제 차이는 perf stat -e L1-dcache-loads,L1-dcache-load-misses로 직접 측정한 뒤 비교하세요. 이 표는 캐시 라인 접근 패턴의 질적 차이만 나타냅니다.
C는 행 우선 배열: C/C++의 2차원 배열은 행 우선(row-major)으로 메모리에 연속 저장됩니다(mat[0][0], mat[0][1], …, mat[0][N-1], mat[1][0], …). 따라서 안쪽 루프를 열(j)로 순회해야 공간적 지역성을 최대화할 수 있습니다. 반대로 Fortran과 NumPy(기본 설정)는 열 우선(column-major)이므로 반대로 작성해야 합니다.

히트와 미스

캐시 히트(hit)는 요청한 데이터가 캐시에 존재하는 경우이고, 미스(miss)는 존재하지 않아 하위 메모리 계층에서 가져와야 하는 경우입니다. 미스는 세 가지로 분류됩니다:

커널에서의 캐시 온도: cold 페이지는 캐시에 없을 가능성이 높은 페이지, hot 페이지는 캐시에 있을 가능성이 높은 페이지를 의미합니다. 페이지 할당자(Page Allocator)의 per-CPU 리스트는 hot/cold 페이지를 구분하여 캐시 효율을 높입니다.
캐시 히트와 미스 — L1·L2·L3 계층별 조회 흐름 캐시 히트와 미스 — 계층별 조회 흐름 CPU 메모리 요청 L1 캐시 확인 ~4 사이클 히트 4~5 사이클 미스 +8 사이클 L2 캐시 확인 ~12 사이클 히트 12~14 사이클 미스 +28 사이클 L3 / LLC 확인 ~40 사이클 히트 30~50 사이클 미스 +160 사이클 DRAM 로드 ~200 사이클 캐시 라인 전체를 L1에 채움 데이터 반환 CPU로 데이터 반환 → 히트율이 낮으면 접근 비용이 급증 (히트율 1% 하락 ≈ 전체 성능 약 1% 저하, 앰달의 법칙) → L3 미스는 가장 비쌈 (DRAM 병목) ※ 사이클 값은 일반적인 예시이며 CPU 마이크로아키텍처·캐시 구성에 따라 달라집니다.

캐시 계층 구조

L1 캐시

L1 캐시는 CPU 코어에 가장 가까운 캐시로, 명령어 캐시(L1I)와 데이터 캐시(L1D)로 분리(Harvard 구조)되어 있습니다. 접근 지연(Latency)은 약 3~4 사이클이며, 코어당 독립적으로 존재합니다.

마이크로아키텍처L1DL1ILLC(L3)LLC 레이턴시
Intel Golden Cove48KB 12-way32KB 8-way소켓 공유 (비포함 NINE) · SKU별 상이~40 cycles
Intel Lion Cove (Lunar Lake)48KB L0 + 192KB L148KB L0 + 192KB L12.5MB L2/코어 (전용)L0 4cyc · L1 9cyc · L2 약 16cyc
AMD Zen 548KB 12-way32KB 8-way32MB/CCD (NINE)~30 cycles
AMD Zen 432KB 8-way32KB 8-way32MB/CCD (NINE)~30 cycles
ARM Neoverse V264KB 4-way64KB 4-way32MB/소켓 (SLC)~40 cycles
Intel Lion Cove 3-Level 캐시 계층: Intel Lunar Lake(Lion Cove)부터 코어 앞쪽에 L0→L1→L2 3단계를 배치했습니다. 기존 48KB L1D는 L0(48KB, Load-to-Use 4사이클)로 내려앉고, 그 뒤에 192KB L1(9사이클)을 추가한 뒤 2.5MB L2로 이어집니다. L2 용량은 코어당 전용이며(Redwood Cove의 2MB에서 증가), 중간 단계가 늘어난 만큼 L2 미스를 줄였습니다. AMD Zen 5도 L1D를 32KB 8-way에서 48KB 12-way로 확장했습니다.

L2 캐시

L2 캐시는 명령어와 데이터를 통합(unified)하여 저장하며, 접근 지연은 약 10~17 사이클입니다. 대부분의 현대 프로세서에서 코어별로 독립적으로 할당되며, 용량은 512KB~6MB 범위입니다.

최신 아키텍처별 L2 캐시:

L3 / LLC (Last-Level Cache)

L3 캐시는 패키지 내 여러 코어가 공유하는 LLC(Last-Level Cache)입니다. 접근 지연은 약 30~40 사이클이며, 용량은 수 MB에서 수백 MB(AMD 3D V-Cache)까지 다양합니다. Intel은 LLC를 코어별 슬라이스로 분산하고 링 버스(Bus)/메시 인터커넥트로 연결하며, AMD는 CCX 단위로 L3를 공유합니다.

포함 / 배제 / NINE 정책

정책특성유효 용량코히런시 비용적용 예
Inclusive L3가 L2/L1 내용을 모두 포함 L3 크기에 의해 제한 (L1+L2 ⊂ L3) 낮음 — 스누프가 L3만 확인 Intel Broadwell 이전
Exclusive 각 레벨에 데이터가 한 곳에만 존재 L1 + L2 + L3 (최대 유효 용량) 높음 — 축출 시 상위/하위 동기화 필요 AMD Zen1~Zen3
NINE Non-Inclusive Non-Exclusive. L3 축출이 L2를 무효화(Invalidation)하지 않음 L3 ~ L1+L2+L3 사이 (워크로드 의존) 중간 — 스누프 필터(Snoop Filter) / 프로브(Probe) 필터(Probe Filter) 필요 Intel Skylake-SP+, AMD Zen4+

Exclusive → NINE 전환 (AMD Zen4): AMD는 Zen1~Zen3까지 Exclusive 정책을 사용하여 유효 캐시 용량을 극대화했습니다. Zen4부터 NINE으로 전환한 이유는 L3 축출 시 L2 백 인밸리데이션(Back-Invalidation)의 트래픽 비용이 코어 수 증가와 함께 높아졌기 때문입니다. NINE에서는 L3에서 축출된 라인이 L2에 여전히 존재할 수 있어 불필요한 리필(Refill)을 방지합니다.

Inclusive → NINE 전환 (Intel Skylake-SP): Intel은 서버 프로세서에서 코어 수 증가로 Inclusive L3의 유효 용량이 부족해지자 Skylake-SP부터 NINE으로 전환했습니다. 코어당 L3 슬라이스에 Snoop Filter(캐시 라인 위치 태그)를 추가하여 코히런시 비용을 관리합니다. 커널은 캐시 포함 정책(Inclusive/NINE)을 CPUID 기능 비트로 직접 탐지하지 않습니다. 대신 CPU family/model/stepping 식별을 통해 마이크로아키텍처를 판별하고, cacheinfo 서브시스템에서 cpuid4_info_regs(CPUID leaf 4)를 파싱하여 /sys/devices/system/cpu/cpu*/cache/index*/size를 통해 실제 용량을 노출합니다.

캐시 미스 시 아래 레벨로 조회 — 포함·배제·NINE 정책별 캐시 라인 사본 위치 캐시 미스 시 아래 레벨로 조회 — 사본 위치는 정책마다 다르다 세 정책 모두 조회 순서는 동일하고, 같은 캐시 라인(주소)의 사본이 남는 레벨만 다릅니다 CPU Core 메모리 요청 미스 L1 수십 KB 미스 L2 ~MB 미스 L3 / LLC 수십 MB 미스 DRAM GB Inclusive (포함) L1 ⊆ L2 ⊆ L3 Exclusive (배제) 한 레벨에만 존재 (예: L1) NINE (비포함·비배제) L3 축출이 L2를 무효화하지 않음 있음 없음 있음 있음 없음 있음 있음 없음 있음 또는 없음 항상 존재 항상 존재 항상 존재 있음 · 해당 레벨에 사본 존재 없음 · 해당 레벨에 사본 없음 있음 또는 없음 · 워크로드에 따라 달라짐 CPU 캐시 계층 구조 — 코어별 전용 L1·L2와 공유 L3/LLC CPU 캐시 계층 구조 L1·L2는 코어마다 독립적으로 갖고, L3/LCC는 코어들이 함께 쓰는 공통 자원입니다 Core 0 Core 1 Core 2 L1I 명령어 L1D 데이터 L1I 명령어 L1D 데이터 L1I 명령어 L1D 데이터 L2 코어별 전용 L2 코어별 전용 L2 코어별 전용 L3 / LLC 모든 코어가 공유 · 슬라이스 분산 Main Memory DRAM 접근 지연 약 4 사이클 약 12 사이클 약 30~40 사이클 약 200 사이클 ※ 점선 영역은 코어 내부에만 존재하는 캐시이며, 사이클 값은 일반적인 예시이므로 CPU 마이크로아키텍처에 따라 달라집니다
AMD V-Cache: Zen3/Zen4 기반 3D V-Cache는 TSMC의 3D 패키징으로 CCD 위에 64MB SRAM 다이를 적층하여 L3를 최대 96MB(Zen3) / 96~128MB(Zen4)까지 확장합니다. 게임, 데이터베이스 등 작업 집합이 큰 워크로드에서 LLC 미스율을 크게 줄입니다.

ARM 캐시 계층

ARM 프로세서는 제조사와 설계에 따라 캐시 구조가 다양하지만, 고성능 코어(Cortex-A/Neoverse)는 일반적으로 다음 계층을 따릅니다:

레벨Cortex-X4 예시Neoverse V2 예시특징
L1I64KB (4-way)64KB (4-way)코어별 독립
L1D64KB (4-way)64KB (4-way)코어별 독립, VIPT
L21MB (8-way)2MB (8-way)코어별 독립, PIPT
L3 (SLC)8MB (16-way)32MB (16-way)DSU 공유, 슬라이스 분산
Apple Silicon: Apple M 시리즈는 독자적 설계로 L1D 192KB(성능 코어), L2 16~24MB(SLC)를 사용하며, Firestorm/Avalanche 코어가 매우 큰 캐시로 IPC를 극대화합니다.

캐시 연관도

직접 사상 (Direct-Mapped)

각 메모리 블록이 캐시의 정확히 한 위치에만 매핑됩니다. 구현이 간단하고 접근이 빠르지만 conflict miss가 빈번합니다. 현대 프로세서에서는 거의 사용되지 않습니다.

Conflict miss 구체 예시: 총 용량 4 KB, 캐시 라인 64 B인 직접 사상 캐시는 4096 / 64 = 64개 슬롯을 가집니다. 슬롯 번호는 주소의 비트 [11:6]으로 결정됩니다.

캐시: 4 KB 직접 사상, 캐시 라인 64 B → 슬롯 64개
슬롯 번호 = 주소 비트 [11:6]  (= (addr / 64) % 64)

주소 0x0000  → 슬롯 0   ← int a[1024] 시작
주소 0x1000  → 슬롯 0   ← int b[1024] 시작  ⚠ 충돌!
주소 0x2000  → 슬롯 0   ← int c[1024] 시작  ⚠ 충돌!

/* a[0], b[0], a[0], b[0], … 번갈아 읽으면 */
/* 매 접근마다 상대방을 캐시에서 축출 → 캐시 미스 100% */
for (int i = 0; i < N; i++)
    sum += a[i] + b[i];   /* 매 루프마다 미스 2회 */
해결책: (1) 패딩을 추가하여 배열 시작 주소를 어긋나게 배치하거나, (2) N-Way 집합 연관 캐시를 사용합니다. N-Way 캐시는 같은 슬롯(셋)에 N개의 독립 위치(웨이)를 두어 최대 N개의 배열이 충돌 없이 공존할 수 있습니다.

N-Way 집합 연관 (Set-Associative)

현대 캐시의 표준 방식입니다. 캐시를 여러 셋(set)으로 나누고, 각 셋에 N개의 웨이(way)를 둡니다. 메모리 주소는 하나의 셋에 매핑되지만, 그 셋 내 N개 웨이 중 아무 곳에나 저장될 수 있습니다.

셋 인덱스 계산:

셋 수 = 캐시 크기 / (캐시 라인 크기 × 연관도)
셋 인덱스 = (주소 / 캐시 라인 크기) % 셋 수

단계별 주소 조회 과정

예시: 32 KB 8-Way 집합 연관 캐시 (캐시 라인 64 B)에서 주소 0x0001A0C8을 조회하는 과정입니다.

캐시 파라미터:
  크기   = 32 KB = 32768 bytes
  웨이   = 8
  라인   = 64 bytes  →  오프셋 6비트 [5:0]
  셋 수  = 32768 / (64 × 8) = 64  →  인덱스 6비트 [11:6]
  태그   = 나머지 상위 비트 [31:12]

주소: 0x0001A0 = 0000_0000_0000_0001_1010_0000₂
  ┌────────────────┬────────────┬──────────┐
  │ Tag  [31:12]   │ Index [11:6]│ Ofs [5:0]│
  │  0x00001       │  0b001000  │  0b100000 │
  │  (= 1)         │  (= 셋 #8) │  (= 32)  │
  └────────────────┴────────────┴──────────┘

조회 단계:
  ① 인덱스 비트 [11:6] = 8  →  셋 #8 선택
  ② 셋 #8 내 웨이 0~7의 태그를 병렬 비교
       Way 0: Tag=0x00003, Valid=1  → 불일치
       Way 1: Tag=0x00001, Valid=1  → 일치! ← 히트
       Way 2: Tag=0x00000, Valid=0  → 유효하지 않음
       …
  ③ 히트: Way 1의 데이터에서 오프셋 32번째 바이트 반환
  ④ 미스 시: DRAM에서 캐시 라인 로드 → 빈 웨이(또는 LRU 웨이)에 저장
병렬 태그 비교: N-Way 캐시는 셋 내 N개 웨이의 태그를 동시에 비교합니다. 이를 위해 하드웨어에 N개의 비교기(comparator)가 내장됩니다. 연관도가 높을수록 회로 면적·전력이 늘어나므로, 일반적으로 L1은 4~16-way, L3는 16-way 수준에서 절충합니다.
단계별 주소 조회 — 32비트 주소의 Tag·Index·Offset 분해부터 셋 내 웨이 비교까지 1 주소 분해 — 32비트 주소를 Tag · Index · Offset으로 분해 가상 주소 0x0001A0C8 Tag 0000 0000 0000 0001 1010 Index 000011 Offset 001000 [31:12] · 20비트 [11:6] · 6비트 [5:0] · 6비트 0x0001A = 26 0b000011 = 3 0b001000 = 8 2 인덱스가 행을 고름 — Index 000011(=3) → 셋 #3 L1D 32KB · 8-way · 64B 라인 → 64개 셋 중 3번 행을 엽니다 3 셋 #3 — 8개 웨이의 Tag를 병렬로 비교 주소의 Tag 0x0001A와 같은 값을 찾습니다 (하드웨어는 8개 비교기를 동시에 동작) Way Tag Valid 비교 결과 Way 0 0x00005 1 Way 1 0x0001C 1 Way 2 0x0001A 1 ✓ HIT Way 3 0x00000 0 무효 (빈 슬롯) Way 4 0x00011 1 Way 5 0x0001A 0 태그 일치하지만 무효 Way 6 0x00007 1 Way 7 0x00009 1 Tag가 0x0001A인 웨이는 2개지만, Valid=1인 것은 Way 2 하나뿐입니다 — Valid 비트가 최종 판정 기준입니다 4 결과 — HIT · Way 2 (Tag 0x0001A, Valid 1) 일치 Offset 8 → 라인 안에서 9번째 바이트를 사용합니다 · 미스였다면 DRAM에서 64바이트 라인을 읽어 빈 or LRU 웨이에 채웁니다 직접 사상 · 집합 연관 · 완전 연관 주소 매핑 비교 — 인덱스가 같은 4개 배열의 처리 방식 같은 인덱스를 가진 4개 배열이 연관도에 따라 어떻게 배치되는가 직접 사상 (Direct-Mapped) 슬롯 번호 = 주소 비트 [11:6] a b c d 0x0000 · 0x1000 · 0x2000 · 0x3000 4096바이트 간격 → 비트 [11:6] = 0 슬롯 0 a b · c · d 축출 충돌 — 인덱스당 1개만 상주 번갈아 읽으면 매 접근 미스 4-Way 집합 연관 (Set-Associative) 셋 인덱스 = 주소 비트 [11:6] a b c d 0x0000 · 0x1000 · 0x2000 · 0x3000 4096바이트 간격 → 비트 [11:6] = 0 셋 #0 W0 a W1 b W2 c W3 d 셋 #0: 4개 웨이가 충돌 없이 공존 태그 4개 병렬 비교 → 최대 4개 배열 완전 연관 (Fully-Associative) 인덱스 없이 모든 위치를 후보로 사용 a b c d 0x0000 · 0x1000 · 0x2000 · 0x3000 인덱스 없음 → 배치 위치 제한 없음 캐시 전체 a b c d CONFLICT MISS 없음 모든 태그를 비교 → 검색 비용 높음
연관도와 실패 종류: 직접 사상은 구현이 단순하지만 특정 주소 패턴에서 conflict miss(서로 다른 태그가 같은 슬롯 경쟁)가 발생하고, 집합 연관·완전 연관은 이를 줄이되 회로 복잡도가 늘어납니다. Capacity miss(작업 집합이 캐시보다 큼)와 Compulsory miss(최초 접근)는 연관도와 무관하게 발생합니다.

완전 연관 (Fully-Associative)

메모리 블록이 캐시의 어느 위치에나 저장될 수 있습니다. Conflict miss가 없지만 검색 비용이 높아 TLB 같은 소규모 캐시에 주로 사용됩니다.

태그 / 셋 / 오프셋(Offset) 비트 분해

물리 주소(Physical Address)는 세 영역으로 분해됩니다:

필드비트 수 (예: 32KB 8-way, 64B line)용도
Offset6 (log2(64))캐시 라인 내 바이트 위치
Set Index6 (log2(64 sets))캐시 셋 선택
Tag나머지 비트캐시 라인 식별
/* arch/x86/kernel/cpu/cacheinfo.c — ci_leaf_init() */
static void ci_leaf_init(struct cacheinfo *this_leaf,
                         struct _cpuid4_info_regs *base)
{
    this_leaf->level        = base->eax.split.level;
    this_leaf->type         = base->eax.split.type;
    this_leaf->coherency_line_size = base->ebx.split.coherency_line_size + 1;
    this_leaf->ways_of_associativity = base->ebx.split.ways_of_associativity + 1;
    this_leaf->size = this_leaf->number_of_sets *
                      this_leaf->coherency_line_size *
                      this_leaf->ways_of_associativity;
}
sysfs 확인: /sys/devices/system/cpu/cpu0/cache/index0/에서 ways_of_associativity, number_of_sets, coherency_line_size, size 등을 확인할 수 있습니다.

캐시 인덱싱 방식 (VIVT / VIPT / PIPT)

캐시 주소 변환 방식은 인덱스(Index)와 태그(Tag)를 가상/물리 주소 중 어떤 것으로 계산하는지에 따라 세 가지로 나뉩니다. 이 방식에 따라 TLB와의 파이프라인(Pipeline) 관계, 컨텍스트 스위치 비용, 캐시 앨리어싱 문제가 달라집니다.

VIVT (Virtually-Indexed Virtually-Tagged)

인덱스와 태그 모두 가상 주소(Virtual Address)로 계산합니다. TLB를 기다리지 않아 가장 빠르지만, 두 가지 심각한 문제가 있습니다:

초기 ARM 프로세서(ARM926 등)에서 사용됐으나 현대 설계에서는 거의 사용하지 않습니다.

VIPT (Virtually-Indexed Physically-Tagged)

인덱스는 가상 주소, 태그는 물리 주소로 계산합니다. TLB와 캐시 접근을 병렬로 시작하여 성능을 유지하면서 Homonym 문제를 해결합니다. x86 L1D와 ARM Cortex-A L1D가 이 방식을 사용합니다. 다만 Cortex-A76/X1과 Neoverse N1 계열 L1D는 인덱스 비트 수가 페이지 오프셋을 넘지 않도록 설계되어, 규격상 VIPT이면서 실제로는 PIPT처럼 동작(앨리어싱 없음)합니다.

앨리어싱 회피 조건:

인덱스 비트 수 ≤ page_offset_bits (= log2(페이지 크기))

예: 페이지 크기 4KB (12비트), 캐시 라인 64B (6비트)
  셋 수 = 캐시 크기 / (라인 크기 × 웨이 수)
  인덱스 비트 = log2(셋 수) = log2(캐시 크기 / (64 × N))

  32KB 8-way: 셋 = 32768 / (64×8) = 64   → 인덱스  6비트 ≤ 12비트 → 앨리어싱 없음
  64KB 4-way: 셋 = 65536 / (64×4) = 256  → 인덱스  8비트 ≤ 12비트 → 앨리어싱 없음
  4MB  8-way: 셋 = 4194304 / (64×8) = 8192 → 인덱스 13비트 > 12비트 → 앨리어싱 발생

  (64KB 페이지를 쓰면 page offset이 16비트가 되므로 4MB 8-way도 안전해진다.)

인덱스 비트가 페이지 오프셋 비트보다 많으면 캐시 앨리어싱(aliasing)이 발생합니다. 같은 물리 페이지를 서로 다른 가상 주소로 매핑할 때, 인덱스가 달라져 캐시에 동일 데이터의 복사본이 두 곳에 생기는 문제입니다.

PIPT (Physically-Indexed Physically-Tagged)

인덱스와 태그 모두 물리 주소로 계산합니다. TLB 변환이 완료된 후 캐시를 접근하므로 앨리어싱 문제가 없습니다. ARM L2/L3(Cortex-A, Neoverse), x86 L2/L3가 사용합니다. 변환 대기 지연이 있지만, L2/L3는 L1보다 지연이 크므로 이 오버헤드(Overhead)가 상대적으로 작습니다.

PIPT 조회 흐름 — 캐시 인덱스와 태그가 모두 물리 주소에서 나오므로 TLB 변환이 끝나야 캐시를 인덱싱할 수 있다 PIPT — TLB 변환이 끝나야 캐시를 인덱싱할 수 있습니다 인덱스와 태그를 모두 물리 주소에서 계산하기 때문에, 캐시 조회는 TLB 변환이 끝난 뒤에만 시작할 수 있습니다 1 가상 주소 (VA) — 4 KiB 페이지 기준 VA[63:12] · VPN 가상 페이지 번호 — TLB 로 변환 VA[11:0] · 페이지 오프셋 TLB 를 거치지 않고 그대로 통과 VPN 그대로 통과 2 TLB 조회 — VPN → PFN 변환 TLB (Translation Lookaside Buffer) 가상 페이지 프레임 번호 → 물리 페이지 프레임 번호 (PFN) PFN 3 물리 주소 (PA) 분해 예: 1 MB · 8-way · 64 B 라인 → 2048개 셋 PA[63:17] · Tag PFN[51:5] · 47비트 PA[16:6] · Index PFN[4:0] + 오프셋[11:6] PA[5:0] · Offset 오프셋[5:0] 와 동일 Index[16:6] = PFN[4:0] + 오프셋[11:6] 인덱스에 PFN 비트가 필요 — TLB 응답 후 계산 4 캐시 접근 TLB 대기 중 비교기는 유휴 태그 비교 (Tag Compare) 셋 안 모든 웨이의 태그를 병렬로 비교 셋 선택 (Set Select) PA[16:6] 로 행 선택 TLB 대기 구간 비교기 유휴 · 조회 보류 5 결과 태그 일치 여부와 오프셋으로 반환할 바이트가 결정됩니다 HIT — 태그가 일치한 웨이에서 Offset 가 가리키는 바이트를 반환 MISS — DRAM 에서 64바이트 라인을 읽어 빈·LRU 웨이에 채운 뒤 재조회 인덱스·태그가 모두 물리 주소 기반이라 같은 물리 페이지가 캐시에서 갈라지지 않습니다 — 앨리어싱 없음

커널에서의 앨리어싱 처리

ARM32에는 CONFIG_CPU_CACHE_VIPT로 빌드될 때만 컴파일되는 flush_pfn_alias()가 있어, 사용자 가상 주소의 캐시 컬러에 대응하는 별칭 매핑(alias mapping)을 구성해 그 캐시 라인을 직접 플러시합니다:

/* arch/arm/mm/flush.c — ARM32 aliasing VIPT 캐시 대응 */

#ifdef CONFIG_CPU_CACHE_VIPT
static void flush_pfn_alias(unsigned long pfn, unsigned long vaddr)
{
    unsigned long to = FLUSH_ALIAS_START + (CACHE_COLOUR(vaddr) << PAGE_SHIFT);
    const int zero = 0;

    set_top_pte(to, pfn_pte(pfn, PAGE_KERNEL));
    asm ("mcrr p15, 0, %1, %0, c14\n"
         " mcr p15, 0, %2, c7, c10, 4"
         :
         : "r" (to), "r" (to + PAGE_SIZE - 1 )
         : "r" (zero)
         : "cc");
}
/* flush_icache_alias(), flush_cache_range(), flush_cache_pages() 도 같은 블록에 위치 */
#else
/* 앨리어싱 VIPT가 없는 코어에서는 위 별칭 경로가 컴파일 자체에서 제외됨 */
#endif

/* flush_dcache_folio() 안에서 cache_is_vipt_nonaliasing()이면 실제 라인 플러시를 건너뛴다 */
void flush_dcache_page(struct page *page)
{
    flush_dcache_folio(page_folio(page));
}
EXPORT_SYMBOL(flush_dcache_page);

/* include/asm-generic/cacheflush.h — 아키텍처 독립 기본형(대부분 static inline no-op) */
void flush_dcache_page(struct page *page);
  /* DMA/mmap 후 물리 페이지 캐시 일관성 보장 — aliasing VIPT에서만 실제 라인 플러시 수행 */
인덱싱 방식인덱스태그TLB 대기앨리어싱주요 사용처
VIVT가상가상불필요컨텍스트 스위치마다 플러시초기 ARM (ARM926)
VIPT가상물리병렬 (Tag만)인덱스 비트 > page offset이면 발생x86 L1D, ARM Cortex-A L1D
PIPT물리물리필요 (직렬)없음x86/ARM L2/L3
실무 요점: Arm Cortex-A76/X1과 Neoverse N1 계열의 L1 데이터 캐시는 TRM에 "VIPT which behaves as PIPT"로 기술됩니다. 인덱스 비트 수가 페이지 오프셋 안에 들어가 앨리어싱이 발생하지 않기 때문입니다. 커널도 cache_is_vipt_nonaliasing()으로 이를 판별해 flush_dcache_folio()의 실제 라인 플러시를 건너뜁니다. 반대로 cache_is_vipt_aliasing()이 참인 코어(초기 ARMv7 코어, 임베디드 SoC의 일부 코어)에서는 위 flush_pfn_alias() 경로가 반드시 동작하며, 페이지 컬러에 맞춘 캐시 라인 배치가 필요할 수 있습니다.

캐시 교체 정책

LRU / Pseudo-LRU

캐시 셋이 가득 찼을 때 어떤 라인을 축출할지 결정하는 정책입니다.

적응형 교체

커널 관점: 교체 정책은 하드웨어가 결정하므로 커널이 직접 제어하지 않습니다. 다만 Intel RDT의 CAT(Cache Allocation Technology)를 통해 각 코어/태스크(Task)가 사용할 수 있는 캐시 웨이를 제한할 수 있습니다.

RRIP / SHIP — 현대 캐시 교체 알고리즘

Intel Haswell 이후의 LLC는 단순 LRU/PLRU 대신 더 정교한 교체 알고리즘을 사용합니다:

알고리즘적은 근사상태 비트스캔 저항성사용 사례
LRU기준 (정확한 최근성)way × log2(N) × set낮음소규모 캐시
Pseudo-LRU (PLRU)LRU에 근사트리형 N-1 비트/셋낮음x86 L1/L2 (대부분)
RRIPLRU보다 근사2비트/way높음Intel L3 (Haswell+)
SHIPLRU보다 근사RRIP + SHCT높음Intel LLC (Broadwell+)
비교 기준: 각 알고리즘의 정확한 히트율 격차는 벤치마크와 워크로드에 따라 달라집니다. 실제 비교는 perf stat -e cache-references,cache-misses로 구한 IPC·미스율 변화로 직접 측정하세요. 이 표는 상태 비트 오버헤드와 스캔 워크로드 저항성 같은 구조적 특성만 비교합니다.

쓰기 정책

Write-Back

대부분의 현대 프로세서가 기본으로 사용하는 정책입니다. 쓰기 시 캐시만 갱신하고, 더티(dirty) 비트를 설정합니다. 캐시 라인이 축출될 때만 메모리에 기록하므로 메모리 대역폭(Bandwidth)을 절약합니다.

Write-Through

쓰기 시 캐시와 메모리를 동시에 갱신합니다. 코히런시 관리가 간단하지만 쓰기 대역폭을 많이 소모합니다. 일부 임베디드 시스템이나 특수 용도에서 사용됩니다.

Write-Allocate / No-Write-Allocate

Write-Combining (WC)

WC는 캐시를 거치지 않고 쓰기 결합 버퍼(WC buffer)에 쓰기를 모아서 버스트 전송합니다. 프레임버퍼, MMIO 영역 등 순서가 중요하지 않은 비캐시 가능 영역에 적합합니다.

정책쓰기 시 동작캐시 가능용도
Write-Back (WB)캐시만 갱신, 축출 시 기록O일반 메모리 (기본)
Write-Through (WT)캐시 + 메모리 동시 기록O특수 코히런시 요구
Write-Combining (WC)WC 버퍼에 모아서 버스트X프레임버퍼, MMIO
Uncacheable (UC)직접 메모리 접근(DMA)X디바이스 레지스터
/* 프레임버퍼를 Write-Combining으로 설정 */
int set_memory_wc(unsigned long addr, int numpages);
int set_memory_wb(unsigned long addr, int numpages);

/* arch/x86/mm/pat/set_memory.c */
int set_memory_wc(unsigned long addr, int numpages)
{
    return change_page_attr_set(&addr, numpages,
                                cachemode2pgprot(_PAGE_CACHE_MODE_WC),
                                0);
}
PAT 충돌 주의: ioremap_wc()와 set_memory_wc()를 혼용하면 PAT(Page Attribute Table) 엔트리가 충돌할 수 있습니다. 항상 매핑 해제 후 새 타입으로 재매핑하세요.

캐시 코히런시 프로토콜

멀티코어 시스템에서 여러 코어의 캐시가 동일 메모리 주소의 다른 값을 가지면 안 됩니다. 캐시 코히런시 프로토콜은 각 캐시 라인의 상태를 추적하여 일관성을 보장합니다.

MESI 프로토콜

가장 기본적인 코히런시 프로토콜로, 각 캐시 라인은 네 가지 상태 중 하나입니다:

MESI 상태 전이 — 로컬 코어 요청(PrRd/PrWr)은 위쪽 레인에서, 다른 코어의 스누프 요청은 아래쪽 레인에서 상태를 바꾼다 MESI 상태 전이 — 버스 트랜잭션이 상태를 바꾸는 방식 이 코어의 요청과 다른 코어의 스누프 요청이, 같은 캐시 라인의 상태를 서로 다른 방향으로 바꿉니다 아래 4개 상태(I · E · S · M)는 모두 이 코어의 캐시 라인 기준입니다 CPU 트리거 — 이 코어가 직접 요청 (PrRd / PrWr) PrWr → BusRdX write-allocate · 데이터 선취득 PrRd → BusRd 공유자 없음 PrWr 무버스 (silent) PrRd → BusRd 공유자 있음 PrWr → BusUpgr 다른 코어 무효화 I · Invalid 유효 복사본 없음 비트만 유효 E · Exclusive 유일 복사본 · clean 메모리와 동일 S · Shared 공유 복사본 · clean 메모리와 동일 M · Modified 유일 복사본 · dirty 메모리는 오래됨 Snoop Read Write-back 없음 Snoop Write 무효화 Snoop Invalidate 무효화 (BusUpgr 수신) Snoop Read 메모리 Write-back 후 강등 Snoop Write 메모리 Write-back 후 무효화 스누프 트리거 — 다른 코어의 버스 요청에 반응 E 는 읽은 직후 쓰면 버스 트랜잭션 없이 M 으로 올라갑니다 — 읽기 후 쓰기 패턴에서 대역폭을 아낍니다 반면 M 은 다른 코어가 읽을 때마다 메모리로 Write-back 하므로, 여러 코어가 한 라인을 함께 쓰면 메모리 대역폭이 소모됩니다

MOESI (AMD)

AMD는 MESI에 Owned (O) 상태를 추가한 MOESI 프로토콜을 사용합니다. Owned 상태의 코어는 다른 코어들과 데이터를 공유하면서도 수정된 값의 책임을 집니다 — 메모리에 쓰기를 지연시키면서 스누프 요청에 직접 응답할 수 있어, Modified→Shared 전환 시 불필요한 메모리 쓰기를 회피합니다.

상태유효독점수정됨설명
M (Modified)OOO유일 복사본, dirty. 쓰기 즉시 가능.
O (Owned)OXOdirty 데이터의 공급자(Supplier). 다른 코어도 Shared 복사 보유 가능. 메모리 쓰기 지연.
E (Exclusive)OOX유일 복사본, clean. 쓰기 시 M으로 전환 (버스 트랜잭션(Transaction) 불필요).
S (Shared)OXX여러 코어 공유, clean. 쓰기 시 Invalidate 필요.
I (Invalid)XXX무효. 읽기 시 버스 트랜잭션으로 데이터 획득.

MOESI의 핵심 이점은 M→O 전환입니다. 코어 A가 Modified 상태인 데이터를 코어 B가 읽으면, MESI에서는 A가 반드시 메모리에 쓰기(Write-Back)를 수행한 뒤 S 상태로 전환해야 합니다. MOESI에서는 A가 O(Owned) 상태로 전환하면서 B에 직접 데이터를 전달하고, 메모리 쓰기를 생략할 수 있습니다. 이는 프로듀서-컨슈머(Producer-Consumer) 패턴에서 메모리 대역폭을 절약합니다.

MESIF (Intel)

Intel은 MESI에 Forward (F) 상태를 추가한 MESIF 프로토콜을 사용합니다. Shared 상태의 여러 코어 중 하나가 Forward로 지정되어 스누프 요청에 응답하는 역할을 합니다. 이를 통해 Shared 상태에서 여러 코어가 동시에 응답하는 중복을 방지합니다.

F(Forward) 상태는 S(Shared)와 데이터 유효성은 동일하지만, 스누프 응답 책임을 가집니다. 새로운 코어가 동일한 캐시 라인(Cache Line)을 읽으면, F 상태 코어가 유일한 응답자가 되어 데이터를 전달하고, 응답자는 S로 강등되며 요청자가 새로운 F가 됩니다. Intel의 메시(Mesh) 인터커넥트에서 여러 L3 슬라이스(Slice)가 동일 데이터를 가질 때 대역폭 낭비를 방지합니다.

MOESI vs MESIF 비교

비교 항목MOESI (AMD)MESIF (Intel)
추가 상태Owned (O) — dirty 공유Forward (F) — clean 응답 지정
해결하는 문제M→S 전환 시 불필요한 메모리 쓰기S 상태 다중 응답자 중복
M→Share 읽기 시M→O (메모리 쓰기 생략, 직접 전달)M→E (다른 sharer 없음) 또는 M→S (쓰기백 후 공유) — clean point는 LLC 스라이스
대역폭 영향코어간 직접 전달로 메모리 대역폭 절약단일 응답자로 인터커넥트 대역폭 절약
복잡도5 상태 관리, O→I 시 Write-Back 필요5 상태 관리, F 역할 이전 로직
최적 시나리오프로듀서-컨슈머: 한 코어가 쓰고 여러 코어가 읽는 패턴다중 리더: 여러 코어가 동일 데이터를 반복 읽는 패턴
ARM CHI 대응UD→SC + SD 전환과 유사SC 중 F 역할과 유사 (최근 요청자 우선)

스누핑 vs 디렉토리 기반

메커니즘원리확장성구현 예
스누핑모든 코어가 버스 트래픽을 감시(snoop)낮음 — 코어 수 증가에 따라 스누프 트래픽이 함께 증가초기 SMP
Snoop FilterLLC에 태그 디렉토리 유지, 불필요한 스누프 억제중간Intel Skylake-SP+
Probe FilterAMD의 LLC 기반 디렉토리. HT Assist(Probe Filter)로 스누프 트래픽 감소중간AMD Zen
디렉토리 기반중앙 디렉토리가 캐시 라인 위치를 추적높음 (수백 코어)ARM CHI HN-F, Intel CXL

ARM CHI (Coherent Hub Interface)

ARM의 고성능 프로세서(Neoverse, Cortex-A7x+)는 CHI (Coherent Hub Interface) 프로토콜을 사용합니다. CHI는 디렉토리 기반 코히런시로 수백 코어까지 확장 가능합니다.

CHI 구성 요소

CHI 캐시 상태 (MOESI 확장)

CHI는 MOESI를 확장한 7개 상태를 사용합니다:

상태의미MESI 대응
I (Invalid)무효Invalid
UC (Unique Clean)유일 복사본, cleanExclusive
UD (Unique Dirty)유일 복사본, dirtyModified
SC (Shared Clean)공유, cleanShared
SD (Shared Dirty)공유, dirty (다른 노드가 최신값 보유)-
UDP (Unique Dirty Partial)부분 갱신, dirty-
UCE (Unique Clean Empty)할당됐으나 데이터 없음-

CHI 장점

CHI vs x86: x86의 스누프 필터/디렉토리는 점진적 개선이지만, ARM CHI는 처음부터 디렉토리 기반으로 설계되어 대규모 시스템(Neoverse N2 96코어+)에서 더 효율적입니다. 커널은 arch/arm64/mm/cache.S에서 CHI의 캐시 유지보수 명령어를 활용합니다.
커널과 코히런시: 커널이 명시적으로 코히런시를 관리할 필요는 없지만, smp_wmb()/smp_rmb() 같은 메모리 배리어(Memory Barrier)는 코히런시 프로토콜이 전파를 완료하기 전에 다른 코어가 순서가 뒤바뀐 값을 관찰하는 것을 방지합니다.
상세 비교: 아키텍처별 캐시 크기·코히런시 프로토콜 비교표는 CPU 토폴로지 — 캐시 계층 · 코히런시 섹션을 참조하세요.

TLB (Translation Lookaside Buffer)

TLB는 가상→물리 주소 변환 결과를 캐싱하는 특수 캐시로, 페이지 테이블 워크 비용(수십~수백 사이클)을 1~2 사이클로 줄입니다.

TLB 계층

레벨유형엔트리 수 (예: Intel Golden Cove)연관도
L1 DTLB데이터4K: 96, 2M: 32, 1G: 8완전 연관
L1 ITLB명령어4K: 256, 2M/4M: 88-way
L2 STLB통합4K+2M: 204816-way

Hugepage와 TLB Reach

TLB reach는 TLB가 커버할 수 있는 최대 가상 주소 범위입니다:

TLB reach = TLB 엔트리 수 × 페이지 크기
  4K × 2048 = 8MB       /* L2 STLB, 4K 페이지 */
  2M × 2048 = 4GB       /* L2 STLB, 2M hugepage */

Hugepage(2MB/1GB)를 사용하면 동일한 TLB 엔트리 수로 훨씬 넓은 범위를 커버하여 TLB 미스를 크게 줄일 수 있습니다. 커널의 THP(Transparent Huge Pages)는 이를 자동으로 활용합니다.

Hugepage 성능 효과

실제 측정 결과 (4GB 메모리 랜덤 접근 워크로드):

페이지 크기TLB reachdTLB 미스율실행 시간성능 향상
4KB (기본)8MB12.4%8.5초기준
2MB (Huge)4GB0.3%3.2초2.7×
1GB (Gigantic)2TB<0.01%2.9초2.9×
수치 주의 (재현 환경·출처): 위 절대 수치는 특정 환경(단일 소켓 x86, dTLB 4K 미스가 지배적인 랜덤 접근 워크로드)에서의 측정 예시로, 하드웨어·TLB 크기·워크로드에 따라 크게 달라질 수 있습니다. 재현 가능한 불변 관계는 "TLB reach = 엔트리 수 × 페이지 크기"로 페이지 크기를 2MB/1GB로 늘리면 같은 엔트리 수로 커버 범위가 각각 256배/1024배(상수비) 늘어나 dTLB 미스를 줄이는 방향이라는 점입니다. 실제 향상 폭은 perf stat -e dTLB-load-misses로 자기 환경에서 직접 측정하세요.
# Hugepage 효과 측정
# 1) 기본 4K 페이지
echo never > /sys/kernel/mm/transparent_hugepage/enabled
perf stat -e dTLB-load-misses,dTLB-loads ./memory_intensive

# 2) THP 활성화 (2MB)
echo always > /sys/kernel/mm/transparent_hugepage/enabled
perf stat -e dTLB-load-misses,dTLB-loads ./memory_intensive

# 3) 1GB hugepage 할당 (사전 예약 필요)
echo 4 > /sys/kernel/mm/hugepages/hugepages-1048576kB/nr_hugepages
numactl --membind=0 ./memory_intensive_1g
적용 가이드: 데이터베이스, 대용량 해시 테이블(Hash Table), 머신러닝 모델처럼 작업 집합이 수 GB 이상인 워크로드는 hugepage로 TLB 미스율을 수십 배 줄일 수 있습니다. perf stat -e dTLB-load-misses로 미스율이 5% 이상이면 hugepage 적용을 고려하세요.

TLB Shootdown

페이지 테이블을 변경한 후 다른 코어의 TLB에 남아 있는 오래된 매핑을 무효화해야 합니다. 이를 TLB shootdown이라 하며, IPI(Inter-Processor Interrupt)를 사용합니다.

/* mm/tlb.c — TLB 일괄 플러시 */
void flush_tlb_mm_range(struct mm_struct *mm,
                        unsigned long start, unsigned long end,
                        unsigned int stride_shift, bool freed_tables)
{
    /* 로컬 CPU 플러시 */
    if (cpumask_any_but(mm_cpumask(mm), smp_processor_id()) < nr_cpu_ids)
        flush_tlb_others(mm_cpumask(mm), &info); /* IPI 전송 */
    else
        local_flush_tlb();
}

/* PCID (Process Context ID): TLB 태그로 프로세스 구분 → 컨텍스트 스위치 시 전체 플러시 불필요 */
/* ASID (ARM): 동일 목적, 8~16비트 태그 */
TLB shootdown 비용: IPI 기반 TLB shootdown은 수천 사이클이 소요될 수 있습니다. munmap()이나 메모리 해제 시 빈번히 발생하므로, 지나치게 잦은 VMA 조작은 성능 저하의 원인이 됩니다. PCID/ASID를 활용하면 전체 TLB 플러시 대신 선택적 무효화가 가능합니다.

PCID(Process Context ID)와 ASID(Address Space ID)

PCID(x86)와 ASID(Address Space ID)는 동일한 목적을 공유합니다. 컨텍스트 스위치 시 TLB 전체를 플러시하지 않고, 프로세스별 ID만 태그로 구분하여 선택적 무효화가 가능합니다.

특성x86 PCIDARM ASID
비트 폭12비트 (4096개 ID)8비트 (ARMv7), 8/16비트 (ARMv8, 구성 가능)
레지스터 위치CR3 하위 12비트TTBR0_EL1/TTBR1_EL1 상위 ASID 필드
활성화 비트CR4.PCIDE (비트 17)별도 비트 없음 — TTBR 내 ASID 필드로 항상 동작
무효화 명령어INVLPG(단일), INVPCID(범위/전체)TLBI 명령어 패밀리는 16종 이상

Linux 커널 PCID 활용

Linux x86에서는 CR3를 적재할 때 CR3의 하위 12비트에 PCID를 함께 적재합니다. cr4_set_bits(X86_CR4_PCIDE)를 통해 활성화됩니다.

/* arch/x86/mm/tlb.c — INVPCID 기반 선택적 TLB 무효화 */
/*
 * INVPCID type (Intel SDM Vol 3A, §4.10.5):
 *   0 = individual address   (특정 PCID + VA)
 *   1 = single context       (해당 PCID의 전체 TLB 항목)
 *   2 = all incl. global     (전체 TLB, 전역 항목 포함)
 *   3 = all non-global       (전역 항목 제외)
 */
struct invpcid_desc {
    u16 pcid;
    u16 reserved[3];
    unsigned long addr;
} __packed;

static inline void __invpcid(unsigned long pcid, unsigned long addr,
                       unsigned long type)
{
    struct invpcid_desc desc = { .pcid = pcid, .addr = addr };

    asm volatile("invpcid %[desc], %[type]"
               : /* 출력 없음 */
               : [desc] "m" (desc), [type] "r" (type)
               : "memory");
}

#define INVPCID_TYPE_SINGLE_CTXT 1
static inline void invpcid_flush_single_context(unsigned long pcid)
{
    __invpcid(pcid, 0, INVPCID_TYPE_SINGLE_CTXT);
}

/* arch/arm64/mm/context.c — ASID 기반 TLB 관리 */
/* ASID는 TTBR0_EL1 상위 비트에 태그로 저장되어 TLB 항목을 프로세스별로
 * 구분합니다. 컨텍스트 스위치 시 전체 플러시 대신 ASID만 교체합니다. */
void check_and_switch_context(struct mm_struct *mm)
{
    /* ASID가 만료(롤오버)되었으면 새 ASID 할당 + 전체 TLB 무효화 */
    if (mm->context.id == 0)
        mm->context.id = new_context(mm);

    /* TTBR0_EL1 갱신: pgd + ASID 동시 적재 (cpu_do_switch_mm) */
    cpu_switch_mm(mm->pgd, mm);
}
AMD INVLPGB (Linux 6.15+): Zen3+ 프로세서에서 지원되는 하드웨어 최적화 기능입니다. PCID/ASID와 함께 동작하여 TLB shootdown 시 모든 코어에 동시에 TLB 무효화를 브로드캐스트합니다. 대규모 NUMA 시스템에서 IPI 기반 shootdown 비용(~10,000 cycles)을 브로드캐스트(~500-1000 cycles)로 크게 절감합니다. 커널은 arch/x86/mm/tlb.c에서 CPUID 비트 X86_FEATURE_INVLPGB를 감지하여 자동으로 활성화합니다.

캐시 프리페칭

하드웨어 프리페처

현대 프로세서는 메모리 접근 패턴을 감지하여 자동으로 데이터를 미리 캐시에 로드합니다. 하드웨어 프리페처는 다양한 형태로 동작합니다.

프리페처지원 아키텍처동작설정 방법
L1D Stride Prefetcher Intel (Golden Cove+) 일정한 stride 패턴을 4~8개까지 학습. 배열 순회, 반복 루프에서 효과적입니다. MSR 0x1A4 (IA32_PMC_CTL0/1) 또는 MSR 0x1A8 (IA32_L1PF_CTL0) — L1PF_L1D_STRIDE_ENABLE 비트
L1D Stride Prefetcher (2nd) Intel (Golden Cove+) 동시 2개의 stride를 학습합니다. 중첩 루프에서 효과적인 프리페치 패턴입니다. MSR 0x1A9 (IA32_L1PF_CTL1) — L1PF_L1D_STRIDE_2ND_ENABLE
L2 Stride Prefetcher Intel (Golden Cove+) L2 캐시 수준에서 stride를 감지. stride가 256B~8KB 범위인 대규모 순환 패턴에 효과적입니다. MSR 0x1A2 (IA32_L2PF_CTL) — L2PF_L2_STRIDE_ENABLE
Adjacent Cache Line Prefetch Intel (Golden Cove+) L1D 스트림 접근 시 인접 캐시 라인(pair)을 함께 로드. 공간 지역성(spatial locality) 활용 MSR 0x1A2 — L2PF_L2_STREAMER_ENABLE (L2 스트리머와 연동)
L2 Streamer Intel (Golden Cove+) L2 미스를 모니터링하며 최대 20개 캐시 라인을 ahead로 프리페치. 연속 메모리 접근 시 강력한 성능 개선을 보입니다. MSR 0x1A2 — L2PF_L2_STREAMER_ENABLE (기본 ON)
L2 Stride Prefetcher (AMD) AMD (Zen3+) L2에서 stride 패턴을 감지. stride는 64B~2048B 범위입니다. MSR 0xC0011020 — PrefetchControl.L2SP (bit 3)
프리페처 튜닝: 하드웨어 프리페처는 일반적으로 기본 활성화된 상태입니다. 특정 워크로드에서는 stride prefetcher를 비활성화해야 오히려 성능이 개선되는 경우가 있습니다. stride가 너무 작거나 불규칙한 접근 패턴에서 프리페처 오버서브스크립션(oversubscription)이 발생할 수 있습니다. wrmsr 명령어로 MSR을 수정하여 테스트할 수 있지만, 재부팅 시 초기화됩니다.

소프트웨어 프리페치

커널은 명시적 프리페치 명령으로 하드웨어 프리페처를 보완합니다:

/* include/linux/prefetch.h */
#define prefetch(x) __builtin_prefetch(x, 0, 3)  /* 읽기, 높은 시간적 지역성 */
#define prefetchw(x) __builtin_prefetch(x, 1, 3) /* 쓰기, 높은 시간적 지역성 */

/*
 * __builtin_prefetch(addr, rw, locality)
 *   rw:       0 = 읽기, 1 = 쓰기
 *   locality: 0 = NTA(비시간적), 1 = T2, 2 = T1, 3 = T0(가장 가까운 캐시)
 */
x86 명령어GCC locality동작
PREFETCHT03L1 + L2 + L3로 프리페치
PREFETCHT12L2 + L3로 프리페치
PREFETCHT21L3로 프리페치
PREFETCHNTA0비시간적(Non-Temporal), 캐시 오염 최소화

커널 사용 사례

네트워크 스택(Network Stack)에서 sk_buff의 다음 패킷(Packet)을 미리 프리페치하여 캐시 미스를 줄이는 패턴:

/* net/core/dev.c — NAPI 폴링에서 프리페치 */
static void skb_defer_free_flush(struct softnet_data *sd)
{
    struct sk_buff *skb, *next;
    llist_for_each_entry_safe(skb, next, ...) {
        prefetch(next);          /* 다음 skb를 미리 캐시에 로드 */
        __kfree_skb(skb);
    }
}
프리페치 주의사항: 과도한 프리페치는 캐시 오염(cache pollution)과 메모리 대역폭 낭비를 초래합니다. 프리페치는 실제로 곧 사용될 데이터에만 적용하고, perf stat으로 효과를 측정한 후 유지 여부를 결정하세요.

AMD Zen4 프리페처와 CLDEMOTE

AMD Zen4 이후의 프리페처와 Intel Tiger Lake 이후의 새 캐시 명령어입니다:

/* CLDEMOTE: 데이터를 하위 캐시 레벨로 내리기 (Intel Tiger Lake+, 2020) */
/* 반대: PREFETCHT0이 데이터를 L1으로 올림, CLDEMOTE는 L1→L2/L3로 내림 */
static inline void cldemote(const void *addr)
{
    asm volatile(".byte 0x0f, 0x1c, 0x07"  /* CLDEMOTE [rdi] */
                 :               : "D" (addr)
                 : "memory");
}

/* 사용 예: 생산자-소비자 패턴에서 생산 완료 후 데이터를 L2로 내려
 * 다른 코어가 L2에서 읽도록 유도. MESI Exclusive → LLC 수준으로 이동 */
void producer_finish(void *item)
{
    /* 데이터 처리 완료 */
    process_item(item);
    /* 소비자 코어가 LLC에서 가져가도록 힌트 */
    cldemote(item);
    /* 소비자에게 알림 */
    enqueue(item);
}
명령어동작 방향캐시 효과지원 CPU
PREFETCHT0메모리 → L1데이터를 L1으로 올림x86 공통
PREFETCHNTA메모리 → L1 (NTA)캐시 오염 최소화x86 공통
CLDEMOTEL1 → L2/L3데이터를 하위 레벨로 내림Intel Tiger Lake+
PREFETCHW메모리 → L1 (쓰기)쓰기 의도 사전 로드, 캐시 라인을 Exclusive 상태로 확보x86 공통 (AMD K6-2+ 도입)

캐시 파티셔닝 — Intel RDT

Intel RDT(Resource Director Technology)는 LLC와 메모리 대역폭을 태스크/컨테이너(Container) 단위로 파티셔닝하는 하드웨어 기능입니다.

CAT (Cache Allocation Technology)

CAT는 LLC(L3) 또는 L2를 CBM(Capacity Bitmask)으로 파티셔닝합니다. 각 비트가 캐시 웨이 그룹을 나타내며, CLOSID(Class of Service ID)별로 다른 CBM을 할당합니다.

# resctrl 마운트
mount -t resctrl resctrl /sys/fs/resctrl

# CBM 구조 확인 (11비트 = 11개 웨이 그룹)
cat /sys/fs/resctrl/info/L3/cbm_mask
# 7ff (0b11111111111)

# 실시간 태스크용 파티션 생성 (상위 4개 웨이 독점)
mkdir /sys/fs/resctrl/rt_group
echo "L3:0=f00" > /sys/fs/resctrl/rt_group/schemata
echo $RT_PID > /sys/fs/resctrl/rt_group/tasks

CDP (Code and Data Prioritization)

CDP는 CAT를 확장하여 코드(명령어)와 데이터에 별도의 CBM을 할당합니다. 코드가 큰 워크로드(JIT 컴파일러 등)에서 데이터 캐시 오염을 방지할 수 있습니다.

# CDP 활성화
mount -t resctrl resctrl /sys/fs/resctrl -o cdp

# 코드에 웨이 0-3, 데이터에 웨이 4-7 할당
echo "L3:0=00f;0=0f0" > /sys/fs/resctrl/jit_group/schemata

MBA (Memory Bandwidth Allocation)

MBA는 메모리 대역폭을 백분율로 제한합니다. noisy neighbor 문제를 완화하여 지연 민감 워크로드를 보호합니다.

resctrl 파일시스템(Filesystem)

경로용도
/sys/fs/resctrl/info/하드웨어 RDT 기능 정보 (CBM 폭, CLOSID 수)
/sys/fs/resctrl/schemata기본 그룹의 캐시/대역폭 할당
/sys/fs/resctrl/tasks기본 그룹 소속 PID 목록
/sys/fs/resctrl/<group>/사용자 정의 CLOSID 그룹
/sys/fs/resctrl/mon_data/LLC 점유율 / 메모리 대역폭 모니터링(CMT/MBM)

AMD PQoS (Platform QoS)

AMD는 Zen3(Milan) 이후 L3 CAT와 MBA를 지원합니다. resctrl 인터페이스는 Intel과 동일하게 사용되지만, 하드웨어 구현에 차이가 있습니다.

파라미터Intel (Xeon Scalable 4세대+)AMD (EPYC 7003+)
CLOSID 수최대 16개최대 16개 (Zen3), 최대 128개 (Zen5)
CBM 세분성L3 웨이 그룹 단위L3 웨이 그룹 단위 (CCD 단위 적용)
L3 CAT 범위전체 소켓(Socket) L3 통합CCD별 독립 L3에 각각 적용
L2 CAT지원 (Xeon SP 일부)Zen4+ 지원
MBA 제어대역폭 백분율 (10% 단위)대역폭 백분율 (10% 단위)
CPUID 감지CPUID.10H (L3 CAT), CPUID.10H.3 (MBA)동일 — CPUID leaf 호환

AMD의 CCD 분리형 L3에서 CAT를 사용할 때 중요한 점은 각 CCD의 L3가 독립적으로 파티셔닝되는 것입니다. resctrl의 schemata 파일에서 L3 도메인(domain) ID가 CCD를 나타내므로, NPS(NUMA Per Socket) 설정에 따라 도메인 수가 달라집니다.

# AMD EPYC에서 CCD별 L3 도메인 확인
cat /sys/fs/resctrl/info/L3/num_closids
# 16

# 도메인별 CBM 할당 (CCD 0과 CCD 1에 서로 다른 파티션)
echo "L3:0=ff0;1=0ff" > /sys/fs/resctrl/rt_group/schemata

RDT 모니터링 — CMT / MBM

Intel RDT와 AMD PQoS는 캐시 할당뿐 아니라 모니터링(Monitoring) 기능도 제공합니다. CMT(Cache Monitoring Technology)는 CLOSID 그룹별 LLC 점유량을, MBM(Memory Bandwidth Monitoring)은 로컬/전체 메모리 대역폭 사용량을 측정합니다.

기능측정 항목resctrl 경로Intel 지원AMD 지원
CMTLLC 점유 바이트mon_data/mon_L3_<domain>/llc_occupancyXeon E5 v4+EPYC 7003+
MBM Total총 메모리 대역폭 (bytes/s)mon_data/mon_L3_<domain>/mbm_total_bytesXeon SP+EPYC 7003+
MBM Local로컬 NUMA 노드 대역폭mon_data/mon_L3_<domain>/mbm_local_bytesXeon SP+EPYC 7003+
# 특정 그룹의 LLC 점유율 모니터링
cat /sys/fs/resctrl/rt_group/mon_data/mon_L3_00/llc_occupancy
# 2457600  (바이트 단위, 약 2.3 MB 점유 중)

# 메모리 대역폭 모니터링 (Total)
cat /sys/fs/resctrl/rt_group/mon_data/mon_L3_00/mbm_total_bytes
# 1073741824  (마지막 리셋 이후 누적 바이트)

# 전체 resctrl 모니터링 데이터 한눈에 보기
find /sys/fs/resctrl/rt_group/mon_data -name "*" -exec sh -c 'echo "{}:"; cat "{}" 2>/dev/null' \;
실전 활용: CMT/MBM은 컨테이너 환경에서 noisy neighbor 탐지에 유용합니다. 특정 CLOSID 그룹의 LLC 점유율이 비정상적으로 높으면 CAT로 해당 그룹의 CBM을 제한하거나, MBA로 메모리 대역폭을 조절하여 다른 워크로드의 성능을 보호할 수 있습니다.

NUMA와 캐시 Affinity

NUMA(Non-Uniform Memory Access) 시스템에서 LLC는 각 소켓(노드)의 코어들과 연결됩니다. 로컬 NUMA 노드의 LLC를 통한 메모리 접근은 빠르지만, 원격 NUMA 노드의 LLC를 경유하거나 원격 DRAM에 접근하면 지연이 크게 증가합니다.

NUMA 접근 지연 비교

메모리 계층접근 지연대역폭 (예: Xeon SP)비고
로컬 L1D~4 사이클—코어 고유
로컬 L2~12 사이클—코어 고유
로컬 L3 (LLC)~40 사이클~4 TB/s소켓 내 공유
원격 LLC (UPI)~130~160 사이클~600 GB/s소켓 간 캐시 라인 이동
로컬 DRAM~80 ns (~280 사이클)~200 GB/sLLC 미스 시
원격 DRAM~140 ns (~490 사이클)~100 GB/s최악의 경우

NUMA 메모리 정책(Memory Policy)과 캐시

커널의 NUMA 정책은 메모리 할당 위치를 결정하며, 이는 캐시 지역성에 직접 영향을 줍니다:

/* include/linux/mempolicy.h — NUMA 메모리 정책 */
#define MPOL_DEFAULT    0  /* 로컬 노드 우선 */
#define MPOL_BIND       2  /* 지정 노드에만 할당 */
#define MPOL_INTERLEAVE 3  /* 노드 간 라운드로빈 */
#define MPOL_PREFERRED  1  /* 선호 노드, 없으면 다른 노드 허용 */
#define MPOL_LOCAL      4  /* 항상 로컬 노드 (strict) */

/* 시스템 콜: 정책 설정 */
int set_mempolicy(int mode, const unsigned long *nmask,
                  unsigned long maxnode);

/* 범위별 정책 (mmap 영역에 적용) */
int mbind(void *addr, unsigned long len, int mode,
          const unsigned long *nmask, unsigned long maxnode,
          unsigned flags);

NUMA 캐시 핫스팟 탐지

# 1) NUMA 통계 개요
numastat -c

# 2) LLC 미스율에서 NUMA 영향 확인
perf stat -e LLC-load-misses,LLC-loads,node-load-misses,node-loads \
  -p $(pgrep myapp) -- sleep 10

# 3) 특정 노드에 프로세스 바인딩
numactl --cpunodebind=0 --membind=0 ./myapp

# 4) NUMA 원격 접근 비율 실시간 모니터링
watch -n 1 'cat /sys/devices/system/node/node*/numastat'

# 5) perf mem으로 원격 NUMA 접근 분석 (PEBS 필요)
perf mem record -a -- sleep 5
perf mem report --sort=mem | head -30
# "Remote LLC" / "Remote DRAM" 항목이 많으면 NUMA 병목
# bpftrace: NUMA 노드별 캐시 미스 집계
bpftrace -e 'hardware:cache-misses:1000 {
    @node[cpu / 4] = count();   /* cpu를 노드로 매핑 (4코어/노드 가정) */
    @comm[comm] = count();
}
END {
    print(@node); print(@comm);
}'
2-NUMA 노드 캐시 계층 — 로컬 코어는 자기 노드의 LLC·DRAM 을 쓰고, 원격 접근은 인터커넥트를 경유해 느려진다 2-NUMA 노드 캐시 계층 — 로컬 접근과 원격 접근 같은 데이터라도 어느 노드의 캐시·메모리를 거치느냐에 따라 접근 비용이 달라집니다 NUMA Node 0 (소켓 0) Core 0 Core 1 Core 2 Core 3 L1/L2 L1/L2 L1/L2 L1/L2 LLC (L3) — Node 0 이 노드의 4개 코어가 공유 DRAM — Node 0 로컬 L3 miss NUMA Node 1 (소켓 1) Core 4 Core 5 Core 6 Core 7 L1/L2 L1/L2 L1/L2 L1/L2 LLC (L3) — Node 1 이 노드의 4개 코어가 공유 DRAM — Node 1 로컬 L3 miss UPI / Infinity Fabric 노드 간 경유 원격 LLC 원격 DRAM 접근 비용 비교 대표값 예시 — 사이클 수와 배율은 소켓·코어 구성에 따라 달라집니다 로컬 LLC ~40c 원격 LLC ~150c 로컬 DRAM ~280c 원격 DRAM ~490c 정확한 배율보다 중요한 것은 순서입니다 — 원격 LLC도 로컬 LLC의 수 배, 원격 DRAM이 가장 느립니다 원격 접근이 늘면 node-load-misses / node-loads 비율이 함께 오릅니다 — numastat -c, perf mem 으로 확인 해결: numactl --cpunodebind=N --membind=N — 코어와 메모리를 같은 노드에 묶습니다
NUMA 성능 함정: 멀티스레드 애플리케이션(Application)에서 스레드(Thread)를 특정 코어에 고정(taskset)하더라도 메모리가 다른 노드에 할당되면 원격 LLC/DRAM 접근이 발생합니다. numactl --cpunodebind=N --membind=N으로 코어와 메모리를 같은 노드에 함께 바인딩하세요.
First-Touch 정책: 리눅스의 기본 NUMA 정책(MPOL_DEFAULT)은 First-Touch 방식으로, 처음 페이지를 사용하는 스레드가 실행 중인 노드에 메모리를 할당합니다. 초기화 스레드와 처리 스레드가 다른 노드에서 실행되면 원격 DRAM에 데이터가 위치하게 됩니다. 자세한 내용은 NUMA를 참조하세요.

False Sharing

발생 메커니즘

두 코어가 같은 캐시 라인에 있는 서로 다른 변수를 독립적으로 수정하면, 코히런시 프로토콜이 캐시 라인 전체를 반복적으로 무효화합니다. 논리적으로 공유가 없지만 물리적으로 캐시 라인을 공유하여 성능이 극심하게 저하됩니다.

/* 문제: counter_a와 counter_b가 같은 캐시 라인에 위치 */
struct shared_counters {
    atomic_t counter_a;  /* CPU 0이 수정 */
    atomic_t counter_b;  /* CPU 1이 수정 → false sharing! */
};

/* 수정: 패딩으로 각 카운터를 별도 캐시 라인에 배치 */
struct shared_counters_fixed {
    atomic_t counter_a;
    char     __pad[L1_CACHE_BYTES - sizeof(atomic_t)];
    atomic_t counter_b;
} ____cacheline_aligned;

탐지

perf c2c는 false sharing을 탐지하는 가장 강력한 도구입니다:

# false sharing 프로파일링
perf c2c record -a -- sleep 10
perf c2c report --stdio

# 출력에서 HITM(Hit in Modified) 비율이 높은 캐시 라인 확인
# Shared Data Cache Line Table에서 문제 변수와 소스 위치 표시

완화

# pahole로 구조체 레이아웃 확인
pahole --class_name task_struct vmlinux | head -50

# 각 필드의 오프셋과 캐시 라인 경계를 표시
false sharing의 비용: 심한 경우 단일 스레드 대비 멀티스레드가 더 느려지는 역설적 상황이 발생합니다. perf stat에서 L1-dcache-load-misses가 비정상적으로 높고 perf c2c에서 HITM이 집중되는 캐시 라인이 있다면 false sharing을 의심하세요.

성능 영향 측정

실제 벤치마크 결과로 false sharing의 성능 저하를 확인할 수 있습니다:

구성처리량(Throughput) (ops/sec)L1 미스율HITM 비율배수
False Sharing (같은 라인)12M45%38%1.0×
패딩(Padding) 적용 (별도 라인)89M8%<1%7.4×
per-CPU 변수156M2%0%13.0×

테스트 환경: Intel Xeon Gold 6248R (24코어), 2개 스레드가 각각 atomic_inc() 1억 회 실행

/* 벤치마크 재현 코드 */
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>

/* Case 1: False Sharing (같은 캐시 라인) */
struct shared_line {
    atomic_int counter_a;
    atomic_int counter_b;  /* 64바이트 미만 간격 → false sharing */
} shared;

/* Case 2: 패딩 적용 (별도 캐시 라인) */
struct padded_line {
    atomic_int counter_a;
    char __pad[64 - sizeof(atomic_int)];
    atomic_int counter_b;
} padded;

void *worker_a(void *arg) {
    for (int i = 0; i < 100000000; i++)
        atomic_fetch_add(&shared.counter_a, 1);
    return NULL;
}

void *worker_b(void *arg) {
    for (int i = 0; i < 100000000; i++)
        atomic_fetch_add(&shared.counter_b, 1);
    return NULL;
}

/* 컴파일: gcc -O2 -pthread false_sharing.c -o false_sharing
 * 측정: perf stat -e cache-references,cache-misses,L1-dcache-load-misses ./false_sharing
 * 진단: perf c2c record ./false_sharing && perf c2c report */

캐시 관리 명령어

CLFLUSH / CLFLUSHOPT

CLFLUSH는 지정된 주소의 캐시 라인을 모든 캐시 계층에서 무효화하고, 더티 라인이면 메모리에 기록합니다. CLFLUSHOPT는 CLFLUSH의 최적화 버전으로 순서 제약이 느슨하여 병렬 플러시가 가능합니다.

CLWB (Cache Line Write Back)

CLWB는 더티 캐시 라인을 메모리에 기록하되, 캐시에서 무효화하지 않습니다. Persistent Memory(PMEM)에서 데이터를 영속 매체에 기록하면서도 캐시 성능을 유지하는 데 핵심적입니다.

WBINVD / INVD

Non-Temporal 스토어

Non-Temporal 스토어(MOVNTI, MOVNTPS 등)는 캐시를 우회하여 메모리에 직접 기록합니다. 대량 데이터 복사 시 캐시 오염을 방지합니다.

명령어동작캐시 무효화순서 보장(Ordering)용도
CLFLUSHWrite-back + InvalidateO직렬화(Serialization)일반 캐시 플러시
CLFLUSHOPTWrite-back + InvalidateO느슨 (SFENCE 필요)병렬 플러시
CLWBWrite-back onlyX (힌트)느슨 (SFENCE 필요)PMEM 영속
WBINVD전체 Write-back + InvalidateO (전체)직렬화리셋, S3 진입
MOVNTINon-temporal 스토어 (캐시 우회)해당 없음느슨 (SFENCE 필요)대량 복사
/* arch/x86/include/asm/special_insns.h */
static inline void clflush(volatile void *__p)
{
    asm volatile("clflush %0" : "+m" (*(volatile char *)__p));
}

static inline void clflushopt(volatile void *__p)
{
    alternative_io(".byte 0x3e; clflush %0",
                   ".byte 0x66; clflush %0",
                   X86_FEATURE_CLFLUSHOPT,
                   "+m" (*(volatile char *)__p));
}

static inline void clwb(volatile void *__p)
{
    volatile struct { char x[64]; } *__v = __p;
    asm volatile(".byte 0x66, 0x0f, 0xae, 0x30"
                 : "+m" (*__v));
}
SFENCE 필수: CLFLUSHOPT, CLWB, Non-Temporal 스토어 후에는 반드시 SFENCE를 실행하여 모든 쓰기가 메모리에 도달했음을 보장해야 합니다. PMEM 시나리오에서 이를 빠뜨리면 정전 시 데이터 손실이 발생합니다.

Persistent Memory (PMEM)와 캐시 관리

Intel Optane DIMM, CXL Type3 메모리 등의 Persistent Memory(PMEM)는 전원이 꺼져도 데이터가 유지되는 바이트 주소 지정 가능 저장 장치입니다. PMEM을 올바르게 사용하려면 CPU 캐시의 영속성(persistence)을 명시적으로 관리해야 합니다.

ADR과 eADR: 전원 장애 안전 도메인

PMEM에서 데이터 영속성은 안전 도메인(persistence domain) 개념으로 정의됩니다:

기술안전 도메인 경계CLWB 필요 여부SFENCE 필요 여부
ADR (Asynchronous DRAM Refresh)메모리 컨트롤러 쓰기 버퍼까지필수필수
eADR (Enhanced ADR)CPU 캐시까지 (L1/L2/LLC 포함)불필요 (캐시도 안전)필요 (순서 보장)

ADR: 전원 장애 시 메모리 컨트롤러의 쓰기 큐까지만 데이터가 안전합니다. CPU 캐시의 더티 라인은 손실됩니다. 따라서 데이터를 영속화하려면 반드시 CLWB → SFENCE 시퀀스로 캐시를 메모리 컨트롤러까지 내려보내야 합니다.

eADR: CPU 캐시 전체가 배터리 백업 도메인에 포함됩니다. CLWB 없이도 캐시에 기록된 데이터가 안전하지만, 순서 보장을 위해 SFENCE는 여전히 필요합니다. Intel Sapphire Rapids, Granite Rapids 일부 구성에서 지원합니다.

CLWB → SFENCE 영속화 패턴

/* 1) 기본 영속화 패턴 (ADR 환경) */
void pmem_persist(const void *addr, size_t len)
{
    const char *ptr = (const char *)addr;
    const char *end = ptr + len;

    /* 각 캐시 라인을 메모리 컨트롤러까지 write-back (캐시 유지) */
    for (; ptr < end; ptr += 64)
        clwb(ptr);  /* CLWB: 캐시 라인 기록, 무효화하지 않음 */

    /* 스토어 순서를 보장 — CLWB 이전 쓰기가 완전히 메모리에 도달 */
    asm volatile("sfence" ::: "memory");
}

/* 2) 커널 PMEM bio 경로 (drivers/nvdimm/pmem.c — Linux v7.2) */
static void pmem_submit_bio(struct bio *bio)
{
    int ret = 0;
    blk_status_t rc = 0;
    bool do_acct;
    unsigned long start;
    struct bio_vec bvec;
    struct bvec_iter iter;
    struct pmem_device *pmem = bio->bi_bdev->bd_disk->private_data;
    struct nd_region *nd_region = to_region(pmem);

    if (bio->bi_opf & REQ_PREFLUSH)
        ret = nvdimm_flush(nd_region, bio);

    do_acct = blk_queue_io_stat(bio->bi_bdev->bd_disk->queue);
    if (do_acct)
        start = bio_start_io_acct(bio);
    bio_for_each_segment(bvec, bio, iter) {
        if (op_is_write(bio_op(bio)))
            rc = pmem_do_write(pmem, bvec.bv_page, bvec.bv_offset,
                iter.bi_sector, bvec.bv_len);
        else
            rc = pmem_do_read(pmem, bvec.bv_page, bvec.bv_offset,
                iter.bi_sector, bvec.bv_len);
        if (rc) {
            bio->bi_status = rc;
            break;
        }
    }
    if (do_acct)
        bio_end_io_acct(bio, start);

    if (bio->bi_opf & REQ_FUA)
        ret = nvdimm_flush(nd_region, bio);

    if (ret)
        bio->bi_status = errno_to_blk_status(ret);

    bio_endio(bio);
}

/* 실제 영속화는 세그먼트 단위 pmem_do_write()/pmem_do_read() 안에서 수행된다 */
static blk_status_t pmem_do_write(struct pmem_device *pmem,
            struct page *page, unsigned int page_off,
            sector_t sector, unsigned int len)
{
    if (unlikely(is_bad_pmem(&pmem->bb, sector, len))) {
        blk_status_t rc = pmem_clear_poison(pmem, to_offset(pmem, sector), len);

        if (rc != BLK_STS_OK)
            return rc;
    }

    flush_dcache_page(page);
    write_pmem(pmem->virt_addr + to_offset(pmem, sector), page, page_off, len);

    return BLK_STS_OK;
}

/* write_pmem()는 memcpy_flushcache()로 복사 — PMEM 지원 아키텍처의 영속화 훅 */
bio 경로의 영속화 위치: pmem_submit_bio() 자체는 arch_wb_cache_pmem()을 직접 호출하지 않습니다. 캐시 플러시는 세그먼트 단위 pmem_do_write()/pmem_do_read()가 담당하며, 쓰기 시에는 flush_dcache_page()로 페이지 캐시 쪽 더티 라인을 먼저 정리한 뒤 write_pmem() → memcpy_flushcache() 경로로 PMEM 에 적습니다. memcpy_flushcache()는 include/linux/string.h에서 __HAVE_ARCH_MEMCPY_FLUSHCACHE로 보호된 기본형(단순 memcpy())이며, PMEM을 아는 아키텍처가 이를 대체합니다(x86에서는 arch/x86/include/asm/string_64.h의 인라인 구현이 상수 크기 복사에 movnti를 사용). bio 수준 영속화는 REQ_PREFLUSH/REQ_FUA 플래그에 따라 nvdimm_flush()로 수행됩니다.
/* 3) libpmem 사용 (PMDK 사용자 공간 라이브러리) */
/* pmem_persist(addr, len)    → 전체 범위를 영속화 (플러시 + 배리어) */
/* pmem_msync(addr, len)      → ADR 보장이 없는 일반 메모리에서의 fallback */
/* pmem_flush(addr, len)      → 플러시만 (배리어를 앱이 직접 수행) */
/* pmem_drain()               → 배리어만 */
/* pmem_is_pmem(addr, len)    → [addr, addr+len) 전체가 PMEM일 때만 1 */
/* pmem_memcpy_persist()      → memcpy + 영속화를 한 번에 처리 */

/* PMDK 권장 판정 흐름 */
if (pmem_is_pmem(pmemaddr, mapped_len))
    pmem_persist(pmemaddr, mapped_len);
else
    pmem_msync(pmemaddr, mapped_len);

커널 DAX (Direct Access) 코드 경로

DAX는 파일시스템을 통해 PMEM에 직접(Page Cache 없이) 접근하는 메커니즘입니다. Page Cache를 우회하여 PMEM 주소에 직접 mmap/read/write를 수행합니다:

/* drivers/dax/super.c — DAX 코어 진입점 (Linux v7.2)
 * include/linux/dax.h에 선언되어 있고, 정의를 fs/dax.c가 아니라
 * DAX 코어가 있는 drivers/dax/super.c가 제공한다. */
long dax_direct_access(struct dax_device *dax_dev, pgoff_t pgoff, long nr_pages,
        enum dax_access_mode mode, void **kaddr, unsigned long *pfn)
{
    long avail;

    if (!dax_dev)
        return -EOPNOTSUPP;

    if (!dax_alive(dax_dev))
        return -ENXIO;

    if (!dax_dev->ops)
        return -EOPNOTSUPP;

    if (nr_pages < 0)
        return -EINVAL;

    /* 디바이스별 ops가 실제 물리/커널 주소를 채운다 */
    avail = dax_dev->ops->direct_access(dax_dev, pgoff, nr_pages,
            mode, kaddr, pfn);
    if (!avail)
        return -ERANGE;
    return min(avail, nr_pages);
}
EXPORT_SYMBOL_GPL(dax_direct_access);

/* dax_flush() — 쓰기 캐시가 켜진 DAX 디바이스만 PMEM write-back을 수행 */
#ifdef CONFIG_ARCH_HAS_PMEM_API
void arch_wb_cache_pmem(void *addr, size_t size);
void dax_flush(struct dax_device *dax_dev, void *addr, size_t size)
{
    if (unlikely(!dax_write_cache_enabled(dax_dev)))
        return;

    arch_wb_cache_pmem(addr, size);
}
#else
void dax_flush(struct dax_device *dax_dev, void *addr, size_t size)
{
}
#endif
EXPORT_SYMBOL_GPL(dax_flush);
조건 방향 주의: dax_flush()는 쓰기 캐시가 비활성화된 경우에만 early return합니다. 즉 dax_write_cache_enabled()가 참이어야 arch_wb_cache_pmem()이 호출됩니다. 조건을 반대로 읽으면 PMEM 쓰기 캐시를 못 쓰는 디바이스에서만 플러시를 수행하는 잘못된 코드가 됩니다.

x86의 실제 영속화 구현 — arch_wb_cache_pmem()와 memcpy_flushcache()

arch_wb_cache_pmem()은 아키텍처마다 다릅니다. x86에서는 arch/x86/lib/usercopy_64.c의 clean_cache_range()를 그대로 호출하는 래퍼이고, 그 내부에서 clwb()를 캐시 라인 단위로 실행합니다. clwb() 자체(arch/x86/include/asm/special_insns.h)는 CPU 지원에 따라 ds clflush → clflushopt → clwb 순으로 대체되는 인라인 어셈블리입니다.

/* arch/x86/lib/usercopy_64.c — Linux v7.2 */
static void clean_cache_range(void *addr, size_t size)
{
    u16 x86_clflush_size = boot_cpu_data.x86_clflush_size;
    unsigned long clflush_mask = x86_clflush_size - 1;
    void *vend = addr + size;
    void *p;

    for (p = (void *)((unsigned long)addr & ~clflush_mask);
         p < vend; p += x86_clflush_size)
        clwb(p);
}

void arch_wb_cache_pmem(void *addr, size_t size)
{
    clean_cache_range(addr, size);
}
EXPORT_SYMBOL_GPL(arch_wb_cache_pmem);
arch_wb_cache_pmem()에는 SFENCE가 없습니다: 위 구현은 clwb() 루프만 수행하며 sfence()를 실행하지 않습니다. 커널의 bio/DAX PMEM 경로(drivers/nvdimm/pmem.c, drivers/dax/super.c)도 arch_wb_cache_pmem() 호출 뒤에 sfence()를 넣지 않습니다. 따라서 "CLWB → SFENCE" 순서를 직접 코드로 작성해야 하는 것은 사용자 공간(PMDK pmem_persist()) 또는 커널에 없는 직접 경로이며, 커널 PMEM 드라이버는 페이지 쪽 flush_dcache_page()와 bio 플래그(REQ_PREFLUSH/REQ_FUA)를 조합해 처리합니다.

한편 write_pmem()이 쓰는 memcpy_flushcache()는 다른 경로입니다. 상수 크기 복사는 인라인 movnti로 처리하고, 그 외에는 arch/x86/lib/usercopy_64.c의 아웃라인 함수 __memcpy_flushcache()가 담당합니다. 이 함수는 정렬을 맞춘 뒤 movnti 루프(32바이트 → 8바이트 → 4바이트 순)로 스트리밍 스토어하고, 남은 바이트는 memcpy() + clean_cache_range()로 마무리합니다.

/* arch/x86/lib/usercopy_64.c — Linux v7.2 (앞뒤 생략) */
void __memcpy_flushcache(void *_dst, const void *_src, size_t size)
{
    unsigned long dest = (unsigned long) _dst;
    unsigned long source = (unsigned long) _src;

    /* 목적지를 8바이트 경계에 맞춘 뒤 그 구간만 캐시 경로로 복사 */
    if (!IS_ALIGNED(dest, 8)) {
        size_t len = min_t(size_t, size, ALIGN(dest, 8) - dest);

        memcpy((void *) dest, (void *) source, len);
        clean_cache_range((void *) dest, len);
        dest += len;
        source += len;
        size -= len;
        if (!size)
            return;
    }

    /* 4x8 movnti 루프 → 1x8 movnti 루프 → 1x4 movnti 루프 */
    /* ... 크기별로 movnti 스트리밍 스토어 ... */

    /* 잔여 바이트는 일반 복사 + CLWB */
    if (size) {
        memcpy((void *) dest, (void *) source, size);
        clean_cache_range((void *) dest, size);
    }
}
EXPORT_SYMBOL_GPL(__memcpy_flushcache);
PMEM 영속화 데이터 경로 — ADR 은 캐시가 도메인 밖이라 CLWB 가 필수이고, eADR 은 캐시까지 보존하므로 SFENCE 순서 보장만 남는다 PMEM 영속화 데이터 경로 — CLWB + SFENCE 순서 전원 장애에도 남기려면, 더티 캐시 라인을 영속 도메인 안으로 밀어 넣어야 합니다 ADR — 영속 도메인이 MC 까지만 CPU 캐시가 도메인 밖 전원 장애 지점 영속 도메인 ① Store 프로그램 쓰기 CPU 캐시 L1D · L2 · LLC 전원 장애 시 손실 메모리 컨트롤러 쓰기 큐 ④ PMEM NVDIMM 반영 완료 ② CLWB 필수 ③ SFENCE 필수 eADR — CPU 캐시까지 영속 도메인 CLWB 없이도 캐시 기록이 안전 전원 장애 지점 영속 도메인 ① Store 프로그램 쓰기 CPU 캐시 L1D · L2 · LLC 전원 장애 시에도 보존 메모리 컨트롤러 쓰기 큐 ④ PMEM NVDIMM 반영 완료 ② CLWB 선택 ③ SFENCE 필수 CLWB 와 SFENCE 의 역할 CLWB — 더티 라인을 MC 로 내보냄 메모리 컨트롤러로 write-back 캐시 라인 자체는 그대로 둠 SFENCE — 쓰기 순서·완료를 보장 이전 쓰기가 MC 에 완전히 도달 이후의 전원 장애는 안전 SFENCE 없이 전원 장애가 나면 쓰기 큐에 남은 데이터가 손실됩니다 eADR 환경에서도 SFENCE 는 순서 보장을 위해 필요합니다
CLWB 없는 PMEM 쓰기의 위험: 단순히 memcpy(pmem_addr, src, len)만 하면 데이터가 CPU 캐시에만 존재합니다. 전원 장애(ADR 환경) 시 더티 캐시 라인이 소실됩니다. 사용자 공간에서는 pmem_persist()(또는 clwb() + sfence())를 호출해야 하고, 커널 드라이버에서는 PMEM 쓰기 경로가 이미 영속화(memcpy_flushcache() / arch_wb_cache_pmem())를 처리하므로 직접 arch_wb_cache_pmem()을 호출할 일이 없습니다.
eADR 확인: ndctl list -R로 PMEM 리전의 persistence_domain 필드를 확인하세요. "cpu_cache"이면 eADR, "memory_controller"이면 ADR입니다. eADR 환경에서는 CLWB 없이도 캐시 기록이 안전하지만 SFENCE는 여전히 필요합니다.

커널 캐시 API

캐시 플러시 API

아키텍처 독립적인 캐시 관리 API:

/* include/asm-generic/cacheflush.h */
void flush_cache_all(void);                  /* 전체 캐시 플러시 */
void flush_cache_range(struct vm_area_struct *vma,
                       unsigned long start, unsigned long end);
void flush_cache_page(struct vm_area_struct *vma,
                      unsigned long addr, unsigned long pfn);
void flush_icache_range(unsigned long start, unsigned long end);

메모리 타입 변경 (PAT)

/* arch/x86/mm/pat/set_memory.c */
int set_memory_uc(unsigned long addr, int numpages);  /* Uncacheable */
int set_memory_wc(unsigned long addr, int numpages);  /* Write-Combining */
int set_memory_wb(unsigned long addr, int numpages);  /* Write-Back (기본) */
int set_memory_wt(unsigned long addr, int numpages);  /* Write-Through */
APIPAT 엔트리용도
set_memory_uc()UC디바이스 레지스터 (MMIO)
set_memory_wc()WC프레임버퍼, GPU 메모리
set_memory_wt()WT특수 코히런시 요구
set_memory_wb()WB일반 메모리 (기본)
ioremap_cache()WB캐시 가능 I/O 영역
ioremap_wc()WCWrite-Combining I/O 영역

kmap과 캐시 일관성

VIPT(Virtually-Indexed Physically-Tagged) 캐시를 사용하는 아키텍처(일부 ARM)에서는 같은 물리 페이지가 다른 가상 주소로 매핑될 때 캐시 앨리어싱(aliasing) 문제가 발생할 수 있습니다. kmap()/kunmap()은 이를 고려하여 일관된 매핑을 제공합니다.

DMA 캐시 동기화

DMA 전송 전후에 CPU 캐시와 디바이스 간 일관성을 보장해야 합니다:

/* DMA 방향별 캐시 동기화 */
dma_sync_single_for_cpu(dev, dma_handle, size, DMA_FROM_DEVICE);
  /* 디바이스→CPU 전송 후: 캐시를 무효화하여 새 데이터 읽기 */

dma_sync_single_for_device(dev, dma_handle, size, DMA_TO_DEVICE);
  /* CPU→디바이스 전송 전: 캐시를 플러시하여 메모리에 기록 */
Coherent DMA: dma_alloc_coherent()로 할당된 메모리는 하드웨어 코히런시를 보장하므로 별도 동기화가 불필요합니다. 단, uncacheable로 매핑되어 CPU 접근이 느립니다. 빈번한 CPU 접근이 필요하면 streaming DMA(dma_map_single())와 명시적 동기화를 사용하세요.

실전 진단

perf stat 캐시 이벤트

# L1/LLC 캐시 미스율 측정
perf stat -e cache-references,cache-misses,\
L1-dcache-loads,L1-dcache-load-misses,\
LLC-loads,LLC-load-misses,\
dTLB-loads,dTLB-load-misses \
-- ./workload

# 출력 예시:
#  1,234,567  cache-references
#    123,456  cache-misses        # 10.00% of all cache refs
#  5,678,901  L1-dcache-loads
#    567,890  L1-dcache-load-misses  # 10.00%
#    234,567  LLC-loads
#     23,456  LLC-load-misses     # 10.00%
#  4,567,890  dTLB-loads
#      4,567  dTLB-load-misses    #  0.10%

perf c2c

Cache-to-Cache 전송과 false sharing을 분석하는 전문 도구:

# 시스템 전체 C2C 프로파일링 (10초)
perf c2c record -a -- sleep 10

# 보고서 생성
perf c2c report --stdio

# 주요 확인 항목:
# 1) Shared Data Cache Line Table → HITM 비율이 높은 캐시 라인
# 2) 해당 캐시 라인을 접근하는 소스 코드 위치
# 3) Load/Store 비율과 접근 CPU 분포

eBPF 기반 캐시 분석

eBPF 도구를 사용하면 커널 수준에서 프로세스/코어별 캐시 미스를 실시간(Real-time)으로 분석할 수 있습니다:

bpftrace — 프로세스별 캐시 미스

# 프로세스별 LLC 미스 카운트 (1000개 미스마다 샘플링)
bpftrace -e 'hardware:cache-misses:1000 {
    @misses[comm, pid] = count();
}
END {
    print(@misses);
}'

# L1 dcache 미스 상위 10개 프로세스
bpftrace -e '
hardware:L1-dcache-load-misses:500 {
    @[comm] = count();
}
END {
    print(@, 10);
}'

# 스택 트레이스 포함 LLC 미스 (핫스팟 함수 식별)
bpftrace -e '
hardware:cache-misses:10000 {
    @[ustack()] = count();
}
END {
    print(@, 5);
}'

llcstat (BCC) — 코어/프로세스별 LLC 히트율

# LLC 히트율 통계 (1초 간격, 10회)
llcstat-bpfcc 1 10

# 출력 예시:
# PID    NAME         CPU    REFERENCE   MISS    HIT%
# 1234   mysqld       0      542,312     48,721  91.02%
# 5678   python3      2      123,456     98,765  20.00%
# ← python3의 낮은 히트율: 무작위 메모리 접근 패턴 의심

# 특정 프로세스만 모니터링
llcstat-bpfcc -p $(pgrep myapp)

perf mem — 메모리 접근 지연 분석

# 메모리 접근 지연 기록 (PEBS 또는 ARM SPE 필요)
perf mem record -a -- sleep 5

# 접근 지연 분포 보고서
perf mem report --sort=mem,sym | head -40

# 주요 출력 컬럼:
# Overhead — 지연 샘플 비율
# Memory access — 데이터 출처 (L1/L2/L3/Remote/DRAM)
# Symbol — 접근한 함수
#
# 출력 예시:
#  45.23%  L1 hit         spin_lock
#  28.11%  L2 hit         __kmalloc
#  12.34%  LLC hit        copy_page
#   8.45%  Remote LLC     shared_data_update  ← NUMA 문제!
#   5.87%  Local DRAM     page_fault_handler

# Intel VTune CLI (설치 시)
# vtune -collect memory-access -knob analyze-mem-objects=true -- ./myapp
# vtune -report summary -result vtune_results/
eBPF 분석 워크플로우: 먼저 perf stat으로 전체 캐시 미스율을 확인한 후, 미스율이 높으면 llcstat으로 어떤 프로세스가 원인인지 특정하고, 마지막으로 bpftrace 스택 트레이스나 perf mem으로 정확한 함수와 접근 패턴을 파악합니다.

Valgrind Cachegrind

# 캐시 시뮬레이션 기반 프로파일링 (유저 공간 프로그램)
valgrind --tool=cachegrind ./program
cg_annotate cachegrind.out.<pid>

# 함수별/라인별 캐시 미스 수를 상세히 보여줌
# I1/D1/LL(Last-Level) 미스를 각각 표시

sysfs 캐시 인터페이스

# CPU0의 캐시 정보 확인
for i in /sys/devices/system/cpu/cpu0/cache/index*/; do
  echo "=== $(cat $i/level) $(cat $i/type) ==="
  echo "  size:         $(cat $i/size)"
  echo "  ways:         $(cat $i/ways_of_associativity)"
  echo "  sets:         $(cat $i/number_of_sets)"
  echo "  line_size:    $(cat $i/coherency_line_size)"
  echo "  shared_cpus:  $(cat $i/shared_cpu_list)"
done

# 출력 예시:
# === 1 Data ===
#   size:         48K
#   ways:         12
#   sets:         64
#   line_size:    64
#   shared_cpus:  0,8
# === 1 Instruction ===
#   size:         32K
# === 2 Unified ===
#   size:         1280K
# === 3 Unified ===
#   size:         18432K
#   shared_cpus:  0-7
lstopo 시각화: hwloc 패키지의 lstopo 명령은 캐시 계층을 포함한 전체 CPU 토폴로지를 그래픽으로 시각화합니다. lstopo --of png > topology.png로 이미지를 생성하거나 lstopo-no-graphics로 텍스트 출력을 확인할 수 있습니다.

캐시 미스 실습 예제

다음 예제는 캐시 동작 원리를 직접 관찰하고 측정하는 실습 코드입니다.

공간적 지역성 실험

/* cache_locality.c — 행 우선 vs 열 우선 접근 비교 */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define SIZE 4096

int main() {
    int (*matrix)[SIZE] = malloc(sizeof(int) * SIZE * SIZE);
    long sum = 0;
    struct timespec start, end;

    /* Case 1: 행 우선 (Row-major) — 캐시 친화적 */
    clock_gettime(CLOCK_MONOTONIC, &start);
    for (int i = 0; i < SIZE; i++)
        for (int j = 0; j < SIZE; j++)
            sum += matrix[i][j];
    clock_gettime(CLOCK_MONOTONIC, &end);
    long row_ns = (end.tv_sec - start.tv_sec) * 1000000000L +
                  (end.tv_nsec - start.tv_nsec);
    printf("Row-major: %ld ns\n", row_ns);

    /* Case 2: 열 우선 (Column-major) — 캐시 미스 유발 */
    sum = 0;
    clock_gettime(CLOCK_MONOTONIC, &start);
    for (int j = 0; j < SIZE; j++)
        for (int i = 0; i < SIZE; i++)
            sum += matrix[i][j];  /* stride = SIZE × 4바이트 */
    clock_gettime(CLOCK_MONOTONIC, &end);
    long col_ns = (end.tv_sec - start.tv_sec) * 1000000000L +
                  (end.tv_nsec - start.tv_nsec);
    printf("Column-major: %ld ns (%.1fx slower)\n",
           col_ns, (double)col_ns / row_ns);

    free(matrix);
    return 0;
}

/* 측정 예시 결과:
 * Row-major:    180,000,000 ns (180ms)
 * Column-major: 920,000,000 ns (920ms) — 5.1x slower
 *
 * perf로 확인:
 * $ perf stat -e cache-references,cache-misses,L1-dcache-load-misses \
 *     ./cache_locality
 *
 * Row-major:    L1 미스율 ~8%  (공간 지역성 활용)
 * Column-major: L1 미스율 ~95% (stride가 캐시 라인 크기 초과)
 */

캐시 스래싱 실험

/* cache_thrashing.c — 캐시 셋 충돌 재현 */
#include <stdio.h>
#include <stdlib.h>

#define CACHE_SIZE   (256 * 1024)    /* 256KB L2 캐시 */
#define LINE_SIZE    64
#define ASSOCIATIVITY 8            /* 8-way set associative */
#define NUM_SETS     (CACHE_SIZE / (LINE_SIZE * ASSOCIATIVITY))
#define SET_STRIDE   (NUM_SETS * LINE_SIZE)  /* 같은 셋에 매핑되는 주소 간격 */

int main() {
    char *buf = aligned_alloc(LINE_SIZE, SET_STRIDE * 16);
    volatile char temp;

    /* Case 1: 캐시에 수용 가능 (8개 라인 → 1개 셋의 8-way에 정확히 맞음) */
    for (int iter = 0; iter < 1000000; iter++)
        for (int i = 0; i < 8; i++)
            temp = buf[i * SET_STRIDE];
    printf("8 lines: cache hit (fits in 8-way set)\n");

    /* Case 2: 캐시 스래싱 (9개 라인 → 계속 축출 발생) */
    for (int iter = 0; iter < 1000000; iter++)
        for (int i = 0; i < 9; i++)
            temp = buf[i * SET_STRIDE];  /* 9번째가 1번째를 축출 */
    printf("9 lines: cache thrashing (conflict miss)\n");

    free(buf);
    return 0;
}

/* perf 측정:
 * $ perf stat -e L1-dcache-loads,L1-dcache-load-misses,\
 *   LLC-loads,LLC-load-misses ./cache_thrashing
 *
 * 8 lines:  L1 미스율 ~1%  (모두 캐시에 상주)
 * 9 lines:  L1 미스율 ~99% (매 접근마다 conflict miss)
 */

프리페치 거리 실험

/* prefetch_distance.c — 소프트웨어 프리페치 효과 */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define SIZE (16 * 1024 * 1024)  /* 16M 요소 */
#define STRIDE 16                /* 16개씩 건너뛰며 접근 */

int main() {
    int *arr = malloc(SIZE * sizeof(int));
    long sum = 0;

    /* Case 1: 프리페치 없음 */
    for (int i = 0; i < SIZE; i += STRIDE)
        sum += arr[i];

    /* Case 2: 프리페치 적용 (8 라인 앞을 미리 로드) */
    sum = 0;
    for (int i = 0; i < SIZE; i += STRIDE) {
        __builtin_prefetch(&arr[i + STRIDE * 8], 0, 0);  /* NTA 힌트 */
        sum += arr[i];
    }

    free(arr);
    return 0;
}

/* 프리페치 거리 최적화:
 * - 너무 짧으면: 메모리 지연을 숨기지 못함
 * - 너무 길면: 프리페치된 데이터가 사용 전에 축출됨
 * - 최적값: 메모리 지연(~200 사이클) / 루프 처리량(사이클/iter)
 *
 * 예: 루프가 iter당 10사이클이면 → 200/10 = 20 iter 앞을 프리페치
 */
실습 가이드: 위 예제를 직접 실행하고 perf stat으로 캐시 미스율을 측정해보세요. 시스템의 캐시 크기는 getconf -a | grep CACHE로 확인하여 예제 상수를 조정할 수 있습니다. 컴파일 시 -O2 최적화를 사용하되, 컴파일러가 루프를 제거하지 않도록 volatile이나 결과 출력을 포함하세요.

MESI/MOESI 상태 전이

앞서 개념적으로 소개한 MESI 프로토콜을 이벤트-상태 전이 관점에서 정밀하게 살펴봅니다. 각 전이에는 로컬 CPU 요청(PrRd, PrWr)과 버스/스누프 이벤트(BusRd, BusRdX, BusUpgr, Flush)가 구분됩니다.

MESI 전체 전이 매트릭스

현재 상태이벤트다음 상태버스 트랜잭션(Transaction)비고
IPrRdE 또는 SBusRd다른 캐시 hit → S, miss → E
IPrWrMBusRdX배타적 소유권 획득
SPrRdS—로컬 히트, 버스 미사용
SPrWrMBusUpgrinvalidate만 전송 (데이터 불필요)
EPrRdE—사일런트 히트
EPrWrM—사일런트 업그레이드 (핵심 최적화)
MPrRdM—로컬 히트
MPrWrM—이미 배타적+dirty
MBusRdSFlushdirty 데이터 공급 + 메모리 갱신
MBusRdXIFlush소유권 이전
EBusRdS—공유 전환 (데이터 공급 가능)
SBusRdXI—무효화
E→M 사일런트 업그레이드: MESI의 핵심 이점입니다. Exclusive 상태에서 쓰기 시 버스 트랜잭션이 전혀 발생하지 않습니다. 이것이 MSI 프로토콜 대비 MESI가 훨씬 효율적인 이유입니다. 리눅스 커널의 per-CPU 변수가 높은 성능을 보이는 근본 원인이기도 합니다.
MESI 전체 상태 전이 매트릭스 — 가로가 다음 상태, 세로가 현재 상태이며 16개 조합 가운데 13개가 실제 전이이고 3개는 발생하지 않는다 MESI 전체 상태 전이 매트릭스 가로 = 다음 상태(to), 세로 = 현재 상태(from) — 16개 조합 중 13개가 실제 전이, 3개는 발생하지 않음 현재 ↓ 다음 → I 무효 E 독점 · clean S 공유 · clean M 수정 · dirty I 무효 E 독점 · clean S 공유 · clean M 수정 · dirty — 전이 없음 I 에서 유효 데이터가 없음 I→E · PrRd + BusRd 다른 캐시 miss → 독점 I→S · PrRd + BusRd 다른 캐시 hit → 공유 I→M · PrWr + BusRdX 배타적 소유권 획득 E→I BusRdX 무효화 E→E (유지) PrRd 히트 사일런트 · 버스 미사용 E→S BusRd 공유 전환 (데이터 공급) E→M · PrWr 사일런트 업그레이드 버스 미사용 (핵심 이점) S→I BusRdX / BusUpgr 무효화 (타 CPU 쓰기) — 전이 없음 공유 → 독점 전이 없음 S→S (유지) PrRd 히트 버스 미사용 S→M · PrWr + BusUpgr invalidate 만 전송 M→I BusRdX · +Flush 소유권 이전 + write-back — 전이 없음 더티 상실 없이 불가능 M→S BusRd · +Flush dirty 공급 + 메모리 갱신 M→M (유지) PrRd / PrWr 히트 버스 미사용 로컬 CPU 요청 (PrRd / PrWr) 스누프 / 버스 이벤트 자기 히트 (상태 유지) +Flush 는 dirty 라인을 메모리에 write-back 한 뒤 전이함을 뜻합니다 회색 3곳(I→I, S→E, M→E)은 발생하지 않는 조합 — 위 표에는 없는 전이입니다

MOESI 확장: Owned 상태

AMD 프로세서에서 사용하는 MOESI는 Owned(O) 상태를 추가합니다. Modified 라인을 공유할 때 메모리에 write-back하지 않고 O 상태로 전환하여 dirty 데이터의 캐시 간 직접 전달을 가능하게 합니다.

상태유효배타적더티소유자의미
MOOOO유일한 사본, 메모리보다 새 값
OOXOOdirty 사본의 소유자, 다른 캐시에 S 사본 존재
EOOXO유일한 사본, 메모리와 동일
SOXXX공유 사본, 메모리와 동일
IX———무효
MOESI Owned 상태 — 코어 B의 읽기에 코어 A가 응답할 때, MESI 는 메모리를 경유해 소유권을 내려주고 MOESI 는 dirty 데이터를 캐시 간 직접 넘긴 뒤 O 상태로 소유권을 유지한다 MOESI: Owned(O) — dirty 데이터를 캐시 간 직접 공급 같은 BusRd 를 처리해도, MESI 는 메모리를 거치며 소유권을 내려주고 MOESI 는 캐시에서 바로 넘기며 소유권을 유지합니다 MESI (Intel) 메모리 경유 · 소유권 상실 코어 B 의 읽기 (BusRd) → 코어 A 스누프 코어 A M → S write-back 후 강등 Write-back 메모리에 기록 메모리 (RAM) 메모리에 갱신됨 데이터 반환 메모리에서 읽음 코어 B I → S 데이터 수신 MOESI (AMD) 캐시 간 직접 공급 · 소유권 유지 코어 B 의 읽기 (BusRd) → 코어 A 스누프 코어 A M → O dirty 그대로 보유 메모리 (RAM) stale — 그대로 코어 B I → S 데이터 수신 BusRd 직접 공급 핵심 차이 — MOESI 는 dirty 데이터를 메모리까지 보내지 않고 코어 사이에 바로 넘깁니다 ① 코어 B 의 읽기 요청 (BusRd) 이 코어 A 를 스누프하고, 데이터 경로만 달라집니다 메모리는 dirty 소유권을 완전히 내려놓을 때에야 write-back 로 갱신됩니다
/* arch/x86/include/asm/cacheinfo.h — 코히런시 라인 크기 조회 */
static inline unsigned int cache_line_size(void)
{
    return boot_cpu_data.x86_cache_alignment;
}

/* arch/x86/kernel/cpu/intel.c — MESI 프로토콜 감지 (CPUID) */
if (cpu_has(c, X86_FEATURE_SELFSNOOP))
    pr_info("Self-Snoop supported\n");

/* Self-Snoop: 코어가 자신의 스토어 버퍼를 스누프하여
 * write-back 시 스누프 트래픽을 줄이는 최적화.
 * CPUID.01H:EDX[27] 비트로 확인 */
MESIF(Intel): Intel QPI/UPI 기반 멀티소켓 시스템에서는 MESIF를 사용합니다. Forward 상태는 Shared 라인 중 정확히 하나만 데이터 공급을 담당하여 여러 캐시가 동시에 응답하는 race를 방지합니다.

캐시 라인 내부 구조

캐시 라인은 단순한 64바이트 데이터 블록이 아니라, 태그(tag), 상태 비트, 데이터로 구성된 복합 구조입니다. CPU가 메모리 주소를 캐시에서 찾을 때 주소를 세 필드로 분해합니다.

48비트 물리 주소 분해 — Tag 34비트가 8개 Way와 비교되고, Index 8비트가 256개 Set 중 하나를 고르며, Offset 6비트가 라인 안의 바이트를 고른다 48비트 물리 주소 분해 (64B 라인, 8-way, 256 sets) 비트 폭은 실제 비율이며, 세 필드가 Tag 비교 · Set 선택 · 바이트 선택을 각각 담당합니다 47 14 13 6 5 0 Tag 34비트 · 비트 47:14 ② 8개 Way 와 병렬 비교 전체 48비트 중 70.8% Index 8비트 · 비트 13:6 ① Set 1개 선택 16.7% Offset 6비트 · 5:0 ③ 바이트 선택 12.5% 2 1 3 캐시 어레이 — 256 Set(행) × 8 Way(열) Way0 Way1 Way2 Way3 Way4 Way5 Way6 Way7 Set N ① Index 선택 0x0F2A48 miss 0x3D97B1 miss 0x6ACF13 HIT 0x1B4E60 miss 0x58C2AF miss 0x2D07C5 miss 0x7A1B3E miss 0x4E8D92 miss 나머지 255개 64B 64B 64B 64B 64B 64B 64B 64B 선택된 Way 의 캐시 라인 구성 V(1b) D(1b) MESI(2b) Tag(34b) Data(64B) ③ Offset → Data(64B) 내 바이트 주소 분해 예 0x001A_B3C4_D5E7 Tag 0x6ACF13 · Index 0x57 · Offset 0x27 캐시 크기 계산 256 × 8 × 64B = 128KB Tag = 48 - 8 - 6 = 34비트 VIPT 앨리어싱 기준 Index + Offset > 12비트면 위험 8+6=14 위험 · 6+6=12 안전 Tag — 라인 식별 Index — Set 선택 Offset — 바이트 선택 MESI: M 수정 · E 배타 · S 공유 · I 무효
리눅스 커널에서 캐시 정보 조회: /sys/devices/system/cpu/cpu0/cache/index0/ 디렉토리에서 coherency_line_size, number_of_sets, ways_of_associativity, size 등을 확인할 수 있습니다. 이 정보는 CPUID 명령어(x86) 또는 CLIDR_EL1/CCSIDR_EL1(ARM64)에서 파싱됩니다.
# 캐시 구조 확인 스크립트
for idx in /sys/devices/system/cpu/cpu0/cache/index*; do
    echo "=== $(basename $idx) ==="
    echo "Level: $(cat $idx/level)"
    echo "Type: $(cat $idx/type)"
    echo "Size: $(cat $idx/size)"
    echo "Line size: $(cat $idx/coherency_line_size)"
    echo "Sets: $(cat $idx/number_of_sets)"
    echo "Ways: $(cat $idx/ways_of_associativity)"
done

# 출력 예시 (Intel i7):
# === index0 ===
# Level: 1
# Type: Data
# Size: 48K
# Line size: 64
# Sets: 64
# Ways: 12

perf stat 캐시 프로파일링(Profiling) 실전

캐시 성능 분석에서 perf는 가장 강력한 도구입니다. 여기서는 기본 통계 수집부터 perf c2c, perf mem, bpftrace를 활용한 고급 기법까지 단계별로 다룹니다.

perf stat 캐시 프로파일링 워크플로 — ① 전체 미스율 측정, ② 병목 지점 식별, ③ 병목 종류에 따라 perf mem · perf c2c · Huge Page 로 분기 perf stat 캐시 프로파일링(Profiling) 워크플로 ① 전체 미스율 측정 → ② 병목 지점 식별 → ③ 병목 종류에 맞는 도구 선택 ① perf stat 전체 미스율 1차 측정 HW PMU 카운터 (cache-* 이벤트) perf list 로 이벤트 이름 확인 ② 미스율 분석 L1D · LLC · TLB 중 병목 지점 식별 L1D 미스율 <5% 양호 · 5-15% 검토 · >15% 심각 임계값은 워크로드 · 플랫폼에 따라 달라집니다 ③ 무엇이 높은가? — 병목 종류에 따라 심층 도구를 선택 perf mem · perf c2c 는 Intel PEBS / AMD IBS 하드웨어 지원 필요 (dmesg 에서 확인) 증상 도구 조치 (remedy) LLC 미스 · 지연 ↑ 로컬 LLC 로 끝나지 않음 LLC-load-misses / loads perf mem PEBS/IBS 로드 추적 perf mem record -a -g 로드 지연 · 소스 분석 사이클 L1~4 · L2~12 · L3~40 로컬 RAM~200 · 원격 RAM~300+ [sym,dso] 로 함수 매핑 HITM ↑ (false sharing) 공유 라인에서 코어 경합 Rmt/Lcl HITM 카운터 perf c2c Shared Line Table perf c2c record -a -g 캐시 라인 내 충돌 위치 Rmt/Lcl HITM 높은 라인 감지 패딩 · 필드 이동으로 구조 분리 TLB 미스 ↑ 페이지 변환 비용 지배 dTLB 미스율 증가 Huge Page THP / madvise(MADV_HUGEPAGE) Always · madvise 비교 변환 횟수 자체를 줄임 dTLB + iTLB 미스 완화 텍스트 크기 확인 병행 미스 유형별 권고 L1D 미스 → 자료구조 패딩·정렬 · LLC 미스 → 작업 집합 크기 · NUMA 배치 HITM → false sharing · TLB 미스 → Huge Page · dTLB+iTLB → 텍스트 크기

perf stat 고급 이벤트

# L1/L2/LLC 계층별 상세 분석
perf stat -e \
  L1-dcache-loads,L1-dcache-load-misses,\
  L1-icache-load-misses,\
  l2_rqsts.demand_data_rd_miss,\
  l2_rqsts.all_demand_data_rd,\
  LLC-loads,LLC-load-misses,\
  LLC-stores,LLC-store-misses \
  -- ./workload

# 비율 계산 공식:
# L1D 미스율 = L1-dcache-load-misses / L1-dcache-loads × 100
# L2 미스율 = l2_rqsts.demand_data_rd_miss / l2_rqsts.all_demand_data_rd × 100
# LLC 미스율 = LLC-load-misses / LLC-loads × 100
#
# 권장 임계값:
# L1D 미스율 < 5% → 양호
# L1D 미스율 5-15% → 데이터 구조 검토
# L1D 미스율 > 15% → 심각한 캐시 문제

perf c2c 상세 분석

# perf c2c: Cache-to-Cache 전송 분석 (false sharing 탐지)
perf c2c record -a -g -- sleep 10
perf c2c report --stdio --stats

# 핵심 출력 칼럼:
# Shared Data Cache Line Table:
#  Index  Rmt_hitm  Lcl_hitm  Stores  Offset  Symbol
#  -----  --------  --------  ------  ------  ------
#      0       423       156     892   0x40   my_struct+0x40
#      1        87        34     234   0x00   counter_array+0x0
#
# Rmt_hitm: 원격 NUMA 캐시 히트 (가장 비싼 전송)
# Lcl_hitm: 로컬 소켓 내 캐시 히트 (비교적 저렴)
# Offset: 캐시 라인 내 위치 → false sharing 여부 판단

# 특정 프로세스만 추적
perf c2c record -p $PID -- sleep 5

perf mem 메모리 접근 추적

# 메모리 로드 지연 분석 (PEBS 기반)
perf mem record -t load -- ./workload
perf mem report --sort=mem,sym,dso --stdio

# 출력에서 데이터 소스 확인:
# L1 hit   (~4 cycles)  → 캐시 히트
# L2 hit   (~12 cycles) → L1 미스, L2 히트
# L3 hit   (~40 cycles) → LLC 히트
# LFB hit  (~12 cycles) → Line Fill Buffer 히트
# Local RAM (~200 cycles) → LLC 미스, 로컬 DRAM
# Remote RAM (~300+ cycles) → 원격 NUMA DRAM

bpftrace 캐시 트레이싱

#!/usr/bin/env bpftrace
/* cache_miss_heatmap.bt — LLC 미스를 프로세스별로 집계 */

hardware:cache-misses:1000 {
    @miss[comm, pid] = count();
}

interval:s:5 {
    print(@miss);
    clear(@miss);
}

END {
    clear(@miss);
}

/* 실행:
 * sudo bpftrace cache_miss_heatmap.bt
 *
 * 출력 예:
 * @miss[mysqld, 1234]: 45678
 * @miss[nginx, 5678]: 12345
 */
주의: perf mem과 perf c2c는 Intel PEBS(Processor Event-Based Sampling) 또는 AMD IBS(Instruction-Based Sampling) 하드웨어 지원이 필요합니다. perf mem record이 실패하면 dmesg에서 PEBS 관련 오류를 확인하세요.

Intel RDT: CAT/CDP/MBA 실전

앞서 RDT의 기본 개념을 다루었으므로, 여기서는 실전 운영 시나리오와 커널 내부 구현을 심층적으로 살펴봅니다.

Intel RDT resctrl 아키텍처 — schemata 쓰기로 CLOSID 별 CBM 배열을 설정하고 IA32_PQR_ASSOC 로 어떤 CLOSID 를 쓸지 선택한다. CAT 은 CLOSID 당 마스크 1개, CDP 는 code/data 2개로 마스크 MSR 공간이 재매핑되어 CLOSID 수가 절반이 된다. Intel RDT resctrl 아키텍처 (CAT · CDP · MBA) 설정 경로(schemata → CBM 배열)와 선택 경로(PQR_ASSOC → CLOSID)는 서로 다른 MSR 이다 User Space /sys/fs/resctrl info/ 로 cbm_mask schemata L3CODE:0=00f · MB:0=30 쓰기 — CLOSID 별 할당 요청 tasks PID → 그룹 배치 쓰기 — 태스크의 CLOSID/RMID 지정 mon_data/ llc_occupancy 읽기 읽기 — CMT · MBM 카운터 값 Kernel resctrl · x86 fs/ + arch/ rdtgroup_update_schemata() cat_wrmsr() · mba_wrmsr_intel() CLOSID 별 마스크 배열에 기록 __resctrl_sched_in() resctrl.h 인라인 함수 컨텍스트 스위치마다 PQR_ASSOC 갱신 resctrl_mon RMID 카운터 폴링 IA32_PQRMON_n 읽기 Hardware QoS · MSR Intel SDM IA32_L3_CAT_CDM_MASK_n 0xC90 + n · IA32_MBA_THRTL_BASE 0xD50 + n CLOSID 별 마스크 배열 · 쓰기 IA32_PQR_ASSOC 0xC8F · CLOSID[63:32] · RMID[9:0] 배열을 바꾸지 않고 행만 선택 · 쓰기 IA32_PQRMON_n RMID 별 CMT · MBM 카운터 측정값 · 읽기 CLOSID 값이 가리키는 CBM · MBA 행을 선택 CDP 활성화 시 마스크 MSR 공간 재매핑 (Intel SDM Vol.3B Table 17-20) 마스크 MSR CAT 전용 CDP 사용 시 Mask_0 COS 0 COS 0 .Data Mask_1 COS 1 COS 0 .Code Mask_2 COS 2 COS 1 .Data Mask_3 COS 3 COS 1 .Code COS n 의 마스크 MSR 주소 data(n) = base + (n << 1) code(n) = base + (n << 1) + 1 COS 1개가 마스크 MSR 2개를 쓰므로 사용 가능한 CLOSID 수는 절반이 됩니다 (num_closid /= 2)

Noisy Neighbor 격리(Isolation) 시나리오

# 시나리오: 레이턴시 민감 서비스(rt_app)와 배치 작업(batch) 격리

# 1. resctrl 마운트 (CDP + MBA)
mount -t resctrl resctrl /sys/fs/resctrl -o cdp,mba_MBps

# 2. 하드웨어 역량 확인
cat /sys/fs/resctrl/info/L3/cbm_mask     # fff → 12 ways
cat /sys/fs/resctrl/info/L3/num_closids  # 16
cat /sys/fs/resctrl/info/MB/min_bandwidth # 10 (최소 10%)

# 3. RT 그룹: LLC 상위 8 ways 독점 + MBA 무제한
mkdir /sys/fs/resctrl/rt_group
echo "L3:0=ff0" > /sys/fs/resctrl/rt_group/schemata
echo "MB:0=100" > /sys/fs/resctrl/rt_group/schemata
echo $RT_PID > /sys/fs/resctrl/rt_group/tasks

# 4. Batch 그룹: LLC 하위 4 ways + MBA 30%로 제한
mkdir /sys/fs/resctrl/batch_group
echo "L3:0=00f" > /sys/fs/resctrl/batch_group/schemata
echo "MB:0=30" > /sys/fs/resctrl/batch_group/schemata
echo $BATCH_PID > /sys/fs/resctrl/batch_group/tasks

# 5. 모니터링
cat /sys/fs/resctrl/rt_group/mon_data/mon_L3_00/llc_occupancy
cat /sys/fs/resctrl/batch_group/mon_data/mon_L3_00/mbm_total_bytes

커널 내부: CLOSID 전환

/* arch/x86/kernel/cpu/resctrl/core.c — 컨텍스트 스위치 시 CLOSID/RMID 갱신 */
void resctrl_sched_in(struct task_struct *tsk)
{
    struct resctrl_pqr_state *state = this_cpu_ptr(&pqr_state);
    u32 closid = tsk->closid;
    u32 rmid = tsk->rmid;

    if (state->cur_closid != closid || state->cur_rmid != rmid) {
        state->cur_closid = closid;
        state->cur_rmid = rmid;
        wrmsr(MSR_IA32_PQR_ASSOC, rmid, closid);
    }
}

/* MSR_IA32_PQR_ASSOC (0xC8F): Intel SDM Vol.3 §17 / arch/x86/include/asm/resctrl.h
 * Bits [63:32] — CLOSID (Class of Service ID)
 * Bits [31:10] — Reserved
 * Bits [9:0]   — RMID (Resource Monitoring ID, 하위 10비트)
 *
 * 컨텍스트 스위치마다 이 MSR을 업데이트하여
 * 태스크별 LLC 파티션과 모니터링 그룹을 전환합니다. */
컨테이너 환경: cgroup v2와 resctrl을 결합하면 Kubernetes Pod 단위로 LLC를 파티셔닝할 수 있습니다. Intel의 intel-cmt-cat 사용자 공간(User Space) 도구나 rdt-config로 자동화할 수 있으며, 커널 6.5+에서는 resctrl의 cgroup 통합이 개선되었습니다.

ARM64 캐시 유지보수 명령어

ARM64는 x86의 완전한 하드웨어 코히런시와 달리, 소프트웨어 관리 캐시 유지보수가 필요한 시나리오가 있습니다. 특히 DMA, 자기 수정 코드(self-modifying code), 캐시 속성 변경 시 명시적 캐시 명령어를 사용해야 합니다.

ARM64 캐시 유지보수 명령어 — Clean·Invalidate·Zero 와 PoC·PoU 축으로 명령어를 선택하고, DSB·ISB 로 완료 순서를 보장한다. I-cache 에는 Clean 명령이 없어 자기 수정 코드는 DC CVAU 와 IC IVAU 두 단계로 처리한다. ARM64 캐시 유지보수 명령어 — 무엇을, 언제, 어떤 순서로 x86 은 하드웨어 코히런시에 자동 처리되지만 ARM64 는 유지보수 명령어를 직접 순서대로 내리는 방식이다 1 왜 순서가 중요한가 — 캐시 명령어는 비동기다 캐시 명령어는 비동기 DC CIVAC 등을 내리면 발령만 끝나고 실제 전파·write-back 는 진행 중 DSB 가 완료를 기다린다 이전 메모리·캐시 연산이 끝날 때까지 CPU 가 정지 (barrier) ISB 가 파이프라인을 비운다 이후 명령어 fetch 가 새 코드를 반드시 보게 만든다 2 명령어 선택 매트릭스 — 동작 축 × 도달 지점 축 동작 by VA → PoC 모든 관찰자(CPU·DMA·GPU)가 같은 사본을 보는 지점 by VA → PoU I·D 캐시가 합쳐지는 지점 (같은 클러스터만 실행) Clean — 값을 아래로 밀어낸다 DC CVAC DC CVAU Invalidate — 사본을 버린다 DC IVAC IC IVAU Clean + Invalidate DC CIVAC 해당 없음 Zero — 라인을 제로화 DC ZVA 해당 없음 PoU 행에 Clean 명령이 없는 것이 핵심이다 — I-cache 로는 값을 밀어낼 수 없으므로 자기 수정 코드는 DC CVAU 로 밀어낸 뒤 IC IVAU 로 버리는 2단계가 된다 추가 명령어: DC CIVPA / DC CVPA / DC IVPA (by PA → PoC), DC CVAP (→ PoP), IC IALLU (I-cache 전체 무효화) 3 실전 시퀀스 — 커널이 실제로 내리는 순서 자기 수정 코드 (SMC) · 코드 동적 생성 DC CVAU (ish) ↓ DSB ISH IC IVAU (ish) ↓ DSB ISH → ISB caches_clean_inval_pou() CPU → 장치 (장치가 읽기 직전) DC CVAC (sy) ↓ DSB SY 장치 전송 시작 dcache_clean_poc() 장치 → CPU (CPU 가 읽기 직전) DC CIVAC (sy) ↓ DSB SY CPU 읽기 시작 __pi_dcache_clean_inval_poc() 4 공유성(psync_t) — DMA 는 sy 가 필요하고, 코드 경로는 ish 로 충분하다 sy System — 전체 시스템 DMA·GPU 등 모든 관찰자 ish Inner Shareable — 클러스터 내 같은 클러스터의 코어만 nsh Non-Shareable — 로컬 전용 자기 코어만, 전파 없음

커널 캐시 플러시 코드

/* arch/arm64/mm/cache.S — 데이터 캐시 라인 clean + invalidate */
SYM_FUNC_START(__flush_dcache_area)
    dcache_by_line_op civac, sy, x0, x1, x2, x3
    ret
SYM_FUNC_END(__flush_dcache_area)

/* 매크로 확장:
 * 1. CTR_EL0에서 DminLine (최소 캐시 라인 크기) 읽기
 * 2. 주소를 라인 크기로 정렬
 * 3. DC CIVAC 루프: 시작 ~ 끝 주소까지 라인 단위 반복
 * 4. DSB SY: 모든 캐시 연산 완료 대기
 */

/* arch/arm64/include/asm/cacheflush.h */
static inline void flush_icache_range(unsigned long start, unsigned long end)
{
    /* D-cache clean to PoU → I-cache invalidate → barriers */
    __flush_icache_range(start, end);
}

/* 모듈 로딩 시 사용:
 * 1. DC CVAU로 수정된 코드를 D-cache에서 PoU까지 clean
 * 2. DSB ISH — inner shareable 도메인 동기화
 * 3. IC IVAU로 해당 범위 I-cache 무효화
 * 4. DSB ISH + ISB — 파이프라인 플러시
 */

DMA 시 캐시 동기화

/* arch/arm64/mm/dma-mapping.c — non-coherent DMA 디바이스용 */
void arch_sync_dma_for_device(phys_addr_t paddr, size_t size,
                             enum dma_data_direction dir)
{
    switch (dir) {
    case DMA_TO_DEVICE:
        /* CPU → 디바이스: dirty 데이터를 메모리로 flush */
        __dma_flush_area(phys_to_virt(paddr), size);
        break;
    case DMA_FROM_DEVICE:
    case DMA_BIDIRECTIONAL:
        /* 디바이스 → CPU: stale 캐시 라인 무효화 */
        __dma_inv_area(phys_to_virt(paddr), size);
        break;
    }
}

/* ARM CCI/CCN/CMN을 통한 하드웨어 코히런시가 있으면
 * 이 함수는 NOP이 됩니다 (dev_is_dma_coherent 확인).
 * 대부분의 최신 서버 ARM SoC는 AMBA ACE 기반 HW coherent. */
Inner/Outer Shareable: ARM64에서 DSB의 도메인 지정은 중요합니다. DSB ISH는 같은 클러스터 내 코어만 동기화하고, DSB OSH는 GPU/DMA 엔진까지 포함합니다. DMA 동기화에는 반드시 DSB SY 또는 DSB OSH를 사용해야 합니다.

캐시 컬러링과 페이지 할당

캐시 컬러링(page coloring)은 물리 페이지를 캐시 셋 매핑에 따라 분류하여, 서로 다른 가상 주소가 같은 캐시 셋을 과도하게 경쟁하는 것을 방지하는 기법입니다.

캐시 컬러링: 페이지 → 캐시 셋 매핑 — L3 는 1024 개의 칸(set) 으로 나뉘고 각 칸에는 16 자리(way) 가 있다. 4 KB 페이지는 64 줄이므로 연속된 64 칸에 각 1 줄씩 들어간다. 물리 주소의 set 인덱스 10 비트 중 PFN 쪽 4 비트는 페이지 안에서 변하지 않으므로 이것이 색이 되고, 서로 다른 색을 배정하면 칸 구간이 겹치지 않아 간섭이 사라진다. 캐시 컬러링: 페이지 → 캐시 칸 매핑 한 페이지가 어느 캐시 칸을 쓰는지, 그리고 그 칸을 주체별로 어떻게 나눠 가져가느냐 1 L3 캐시는 "칸 1024 개"이고, 각 칸에는 "자리 16 개"가 있다 예시 구성 — 1 MB L3 · 16-way · 64 B 라인 칸 0 … 칸 1023 한 칸의 16 자리가 꽉 차면 다음 줄이 밀려난다 (대부분 LRU 순으로 골라낸다) · 위 그림은 각 칸의 16 자리 중 3 자리만 표시 물리 주소 한 줄 = 캐시 라인 64 바이트 하나를 가리킨다 way 4비트 칸 안 몇 번째 자리인가 set 인덱스 10비트 1024 칸 중 몇 번째 칸인가 라인 내 6비트 64 B 중 몇 바이트인가 2 4 KB 페이지는 64 줄이다 — 그래서 칸 64 개에 각 1 줄씩 들어간다 페이지가 칸 5 에서 시작한다면 칸 5 하나를 확대하면 — 자리 16 개 5~12 13~20 21~28 29~36 37~44 45~52 53~60 61~68 칸 5 ~ 68 (연속된 64 칸, 각 칸에 1 줄씩) 한 페이지 안에서는 칸마다 1 줄뿐이므로 충돌하지 않는다 즉 충돌은 페이지가 아니라 "칸 안"에서 일어난다 A B C D E F G H I J K L M N O P Q 페이지 Q (17 번째) → 자리 하나를 밀어냄 · conflict miss A ~ P = 서로 다른 16 개 페이지가 이 칸을 함께 쓰는 모습 같은 칸을 쓰는 페이지가 16 개를 넘으면 밀리기 시작한다 3 "색"은 칸 구간을 나눠 주는 비트다 set 인덱스 10 비트를 4 KB 페이지 기준으로 다시 보면 한 페이지 안에서는 바뀌지 않음 매 줄마다 바뀜 PFN[11:4] 물리 프레임 번호 PFN[3:0] 색 po[11:6] set 하위 6 비트 po[5:0] 라인 내 6 비트 set 인덱스 10 비트 = PFN[3:0] (4 비트, 안 바뀜 = 색) ‖ po[11:6] (6 비트, 매 줄마다 바뀜) 색 0 색 1 색 2 색 3 색 4 색 5 색 6 색 7 색 8 색 9 색 10 색 11 색 12 색 13 색 14 색 15 칸 0 ~ 63 칸 448 ~ 511 칸 960 ~ 1023 VM-A 는 색 0~3 → 칸 0~255 VM-B 는 색 4~7 → 칸 256~511 나머지 색 8~15 는 비워 두거나 다른 주체에 배정 두 VM 이 닿는 칸 구간이 겹치지 않으므로 서로의 자리를 밀어낼 수 없다 — 이것이 캐시 컬러링이다 4 메인라인 리눅스의 위치 메인라인 커널 명시적 캐시 컬러링을 하지 않는다 PIPT / VIPT 캐시에서 alias 가능성 낮음 실제 쓰임 Jailhouse · Xen — VM 간 캐시 격리 실시간 파티셔닝 · L3 slice 격리

리눅스에서의 캐시 컬러링

메인라인 리눅스 커널은 명시적 캐시 컬러링을 하지 않습니다. 과거 MIPS와 일부 ARM 아키텍처에서 VIVT 캐시의 alias 방지를 위해 사용했으나, 현대 PIPT/VIPT 캐시에서는 필요성이 줄었습니다. 다만 실시간 시스템이나 연구 목적의 패치(Patch)가 존재합니다.

/* 캐시 컬러링 개념 구현 (pseudo-code, 메인라인 아님) */
#define CACHE_COLORS     (L2_CACHE_SIZE / (L2_WAYS * PAGE_SIZE))
/* 예: 256KB L2 / (4-way * 4KB) = 16 colors */

static inline unsigned int page_color(struct page *page)
{
    return (page_to_pfn(page)) & (CACHE_COLORS - 1);
}

/* MIPS에서의 실제 구현 (arch/mips/mm/c-r4k.c):
 * VIPT 캐시에서 가상 주소와 물리 주소의 색상이
 * 다르면 alias가 발생합니다. 이를 방지하기 위해
 * 같은 색상의 페이지를 할당합니다. */

/* arch/mips/include/asm/page.h */
#ifdef CONFIG_MIPS_CACHE_COLOURING
#define COLOUR_ALIGN(addr, pgoff) \
    ((addr + shm_align_mask) & ~shm_align_mask + \
     (((pgoff) << PAGE_SHIFT) & shm_align_mask))
#endif
Jailhouse/Xen 캐시 컬러링: Jailhouse 하이퍼바이저(Hypervisor)(v0.12+)와 Xen(실험적)은 VM 간 캐시 격리를 위해 캐시 컬러링을 지원합니다. 각 VM에 특정 색상의 물리 페이지만 할당하여 LLC에서의 간섭을 차단합니다. 이는 Intel RDT CAT의 소프트웨어 대안입니다.

Write-Combining 버퍼

Write-Combining(WC)은 연속적인 쓰기 작업을 버퍼에 모아 한 번에 메모리로 전송하는 기법입니다. 주로 MMIO(프레임버퍼, GPU BAR)와 같은 비캐시 가능 영역에서 사용되며, 개별 쓰기마다 버스 트랜잭션을 발생시키는 UC(Uncacheable)보다 훨씬 높은 대역폭을 제공합니다.

PAT/MTRR 설정

/* x86 메모리 타입과 PAT(Page Attribute Table) */

/* 메모리 타입 목록 (IA32_PAT MSR) */
#define _PAGE_CACHE_MODE_WB   0  /* Write-Back (기본, 일반 RAM) */
#define _PAGE_CACHE_MODE_WT   1  /* Write-Through */
#define _PAGE_CACHE_MODE_UC_MINUS 2  /* Uncacheable (MTRR 오버라이드 가능) */
#define _PAGE_CACHE_MODE_UC   3  /* Uncacheable (강제) */
#define _PAGE_CACHE_MODE_WC   4  /* Write-Combining */
#define _PAGE_CACHE_MODE_WP   5  /* Write-Protect */

/* arch/x86/mm/pat/set_memory.c — 메모리 타입 변경 */
int set_memory_wc(unsigned long addr, int numpages)
{
    return change_page_attr_set(&addr, numpages,
        cachemode2pgprot(_PAGE_CACHE_MODE_WC), 0);
}

/* 드라이버에서 프레임버퍼를 WC로 매핑:
 *   ioremap_wc(phys_addr, size)
 * 내부적으로 PAT 엔트리를 WC로 설정합니다.
 *
 * WC 쓰기 규칙:
 * 1. 순서 보장 없음 (SFENCE로 명시적 동기화)
 * 2. 64바이트(라인 크기) 단위로 병합
 * 3. WC 버퍼 가득 차거나 SFENCE/MFENCE 시 flush
 * 4. UC보다 4~8배 높은 쓰기 대역폭
 */

WC 활용 패턴

/* GPU 드라이버에서 WC 사용 예시 (i915) */
static void fill_wc_buffer(void __iomem *wc_ptr, u32 *data, size_t len)
{
    /* 64바이트 단위로 정렬된 쓰기 — WC 버퍼 효율 극대화 */
    while (len >= 64) {
        memcpy_toio(wc_ptr, data, 64);
        wc_ptr += 64;
        data += 16;  /* 16 × 4바이트 = 64바이트 */
        len -= 64;
    }
    /* WC 버퍼 강제 플러시 — 디바이스에 실제 전달 보장 */
    wmb();  /* x86에서는 SFENCE로 컴파일 */
}

/* WC vs UC 성능 비교 (프레임버퍼 4MB 채우기):
 * UC: ~160ms (매 4바이트 쓰기마다 PCI 트랜잭션)
 * WC: ~20ms  (64바이트 burst로 병합)
 * WB: ~5ms   (캐시 + write-back, 가능한 경우)
 *
 * 주의: WC 영역을 읽으면 매우 느립니다.
 * 읽기가 필요하면 shadow buffer(WB)를 유지하세요. */
WC 주의사항: Write-Combining 영역의 읽기 성능은 매우 나쁩니다 (매 접근마다 PCI 트랜잭션). GPU 프레임버퍼 등에서 CPU 읽기가 필요하면 WB 메모리에 shadow copy를 유지하세요. 또한 WC 쓰기는 순서를 보장하지 않으므로, 순서가 중요한 I/O에는 사용하면 안 됩니다.

DMA와 캐시 코히런시

DMA(Direct Memory Access) 엔진은 CPU 캐시를 우회하여 메모리에 직접 접근합니다. 코히런트 DMA(x86, 일부 ARM)에서는 하드웨어가 자동 동기화하지만, 비코히런트 DMA(대부분의 임베디드 ARM)에서는 소프트웨어가 명시적으로 캐시를 관리해야 합니다.

DMA와 캐시 코히런시 — CPU 캐시와 메모리는 같은 주소의 두 벌 사본이고 DMA 엔진은 메모리 쪽 사본만 본다. CPU 가 디바이스로 보낼 때는 clean 으로 값을 메모리에 내려야 하고, 디바이스에서 받아 CPU 가 읽을 때는 clean 과 invalidate 로 CPU 쪽 낡은 사본을 버려야 한다. Linux 는 dma_map_single 후 dma_sync_single_for_device, DMA 전송, dma_sync_single_for_cpu, dma_unmap_single 순으로 이 순서를 명시적으로 요구한다. DMA와 캐시 코히런시 — 왜 명시적 동기화가 필요한가 CPU 캐시와 메모리는 같은 주소의 두 벌 사본이다. DMA 엔진은 그중 메모리 쪽 사본만 본다 1 CPU 는 캐시를 거쳐 접근하고, DMA 엔진은 캐시를 건너뛴다 코히런트 DMA 면 하드웨어가 대신 처리하고, 비코히런트 DMA 면 소프트웨어가 위 두 사본을 맞춰야 한다 CPU 코어 Load · Store L1 / L2 캐시 CPU 는 항상 거친다 메모리 실제 데이터가 있는 곳 DMA 엔진 디바이스 — 캐시 경유 없음 DMA 가 보는 영역 디바이스 DMA 엔진은 CPU 의 캐시를 볼 수도 있고 볼 수도 없다 — 볼 수 없는 구조(대부분의 임베디드 ARM)에서는 아래와 같이 소프트웨어가 두 사본을 직접 맞춰야 한다 2 동기화하지 않으면 어느 쪽이 낡아지는가 같은 버퍼 주소 하나에 대한 두 벌 사본의 상태 상황 CPU 캐시 사본 메모리 사본 실제로 읽은 값 필요한 캐시 연산 CPU → 디바이스 new old old (틀림) clean 디바이스 → CPU old new old (틀림) clean + invalidate 두 경우 모두 "캐시 쪽 사본"이 낡다. 다만 고치는 방향이 반대다 — 앞은 값을 메모리 쪽으로 내려보내고(clean), 뒤는 CPU 쪽 사본을 버린다(invalidate). 동기화하지 않으면 디바이스나 CPU 가 상대가 갱신하기 전 값을 읽는다 — 버퍼 내용 자체가 깨져 보인다 3 그러므로 direction 인자에 따라 하는 캐시 연산이 다르다 CPU → 디바이스 · DMA_TO_DEVICE clean CPU 가 마지막으로 쓴 값을 메모리에 write-back 한다 — 디바이스가 메모리를 읽으므로 디바이스 → CPU · DMA_FROM_DEVICE clean + invalidate CPU 쪽에 남아 있는 낡은 사본을 버려 메모리를 다시 읽게 한다 — CPU 가 캐시를 읽으므로 direction 을 DMA_BIDIRECTIONAL 로 잡으면 양쪽 다 맞춰야 하므로 두 sync 를 모두 넣어야 한다 (방향이 명확하면 한쪽은 생략 가능) 4 Linux 는 이 순서를 API 로 강제한다 — streaming 매핑 dma-map-singles 는 매핑만 하고 캐시를 건드리지 않는다 — sync 는 그 뒤에 따로 부른다 dma_map_single() 매핑만 · 캐시 무관 CPU 가상 주소 → IOVA 로 매핑 dma_sync_*_for_device() clean 최신 값을 메모리에 내림 DMA 전송 메모리만 오감 디바이스가 메모리를 다룸 dma_sync_*_for_cpu() clean + invalidate CPU 가 볼 수 있게 맞춤 dma_unmap_single() 매핑 해제 IOVA 반환 코히런트 할당 dma_alloc_coherent() 를 쓰면 하드웨어가 일관성을 보장하므로 sync 호출이 필요 없다 — 대신 CPU 접근이 느리고 할당 크기가 커진다

코히런트 DMA 할당

/* 코히런트 DMA 버퍼: 캐시 동기화 불필요 */
void *buf;
dma_addr_t dma_handle;

/* 할당: x86에서는 일반 WB 메모리 (하드웨어 코히런트)
 * ARM에서는 uncached 또는 write-combining 매핑 */
buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!buf)
    return -ENOMEM;

/* CPU와 디바이스 모두 동시에 접근 가능
 * 추가 sync 호출 불필요 — but 성능 비용:
 * - ARM non-coherent: uncached → 매 접근마다 메모리 왕복
 * - 소량의 설명자(descriptor)에 적합
 * - 대량 데이터에는 streaming DMA 권장 */

/* 해제 */
dma_free_coherent(dev, size, buf, dma_handle);

스트리밍 DMA 매핑

/* 스트리밍 DMA: 고성능 대량 전송용 */
dma_addr_t dma_addr;

/* 1. 매핑: 캐시 clean/invalidate + IOMMU 매핑 */
dma_addr = dma_map_single(dev, buf, size, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_addr))
    return -EIO;

/* 2. DMA 전송 시작 (디바이스에 dma_addr 전달) */
start_dma_transfer(dev, dma_addr, size);

/* --- 전송 중: CPU는 버퍼에 접근하면 안 됨 --- */

/* 3. 전송 완료 후 CPU가 읽기 전에 동기화 */
dma_sync_single_for_cpu(dev, dma_addr, size, DMA_FROM_DEVICE);
/* → non-coherent: DC CIVAC (clean+invalidate)
 * → coherent (x86): NOP */

/* 4. CPU가 데이터 처리 후 다시 디바이스에 전달하려면 */
dma_sync_single_for_device(dev, dma_addr, size, DMA_TO_DEVICE);
/* → non-coherent: DC CVAC (clean만, dirty→메모리)
 * → coherent (x86): NOP */

/* 5. 최종 해제 */
dma_unmap_single(dev, dma_addr, size, DMA_TO_DEVICE);

DMA 방향별 캐시 연산

DMA 방향map 시 캐시 연산sync_for_cpusync_for_device
DMA_TO_DEVICEclean (dirty→메모리)—clean
DMA_FROM_DEVICEinvalidateinvalidate—
DMA_BIDIRECTIONALclean+invalidateinvalidateclean
x86에서 DMA가 "쉬운" 이유: x86은 PCI Express 트래픽이 LLC(Last-Level Cache)를 스누프하므로 하드웨어 레벨에서 코히런트합니다. 따라서 dma_sync_* 함수가 NOP으로 컴파일됩니다. 반면 대부분의 ARM SoC에서는 실제 캐시 유지보수 명령어가 실행되므로 성능에 영향을 줍니다.

캐시 앨리어싱 버그 사례

캐시 앨리어싱(aliasing)은 서로 다른 가상 주소가 동일한 물리 주소를 참조하면서 다른 캐시 셋에 매핑되어 같은 데이터의 서로 다른 캐시 사본이 존재하게 되는 문제입니다. 이는 VIPT(Virtually Indexed, Physically Tagged) 캐시에서 캐시 크기가 페이지 크기 × associativity를 초과할 때 발생합니다.

VIPT 앨리어싱 조건

캐시 구성Index+Offset 비트페이지 크기앨리어싱이유
32KB 4-way6+6 = 124KB (12비트)없음인덱스가 페이지 오프셋 내
32KB 8-way6+6 = 124KB (12비트)없음인덱스가 페이지 오프셋 내
64KB 4-way8+6 = 144KB (12비트)있음!비트 [13:12]가 VA 의존
64KB 4-way8+6 = 1416KB (14비트)없음큰 페이지로 해결

앨리어싱 감지 코드

/* arch/arm/mm/fault-armv.c — VIPT 앨리어싱 감지 및 처리 */
void update_mmu_cache(struct vm_area_struct *vma,
                     unsigned long addr, pte_t *ptep)
{
    unsigned long pfn = pte_pfn(*ptep);
    struct page *page;
    struct address_space *mapping;

    if (!pfn_valid(pfn))
        return;
    page = pfn_to_page(pfn);

    /* 페이지가 여러 VA에 매핑되어 있으면 앨리어스 체크 */
    mapping = page_mapping_file(page);
    if (mapping) {
        int aliases = page_mapped_in_vma(page, vma);
        if (aliases > 1) {
            /* 앨리어싱 감지: 모든 매핑에 대해 캐시 플러시 */
            flush_dcache_page(page);
        }
    }
}

/* flush_dcache_page()는 아키텍처별 구현:
 * - ARM VIPT: 물리 주소 기반으로 전체 앨리어스된 VA flush
 * - x86 PIPT: NOP (물리 인덱스이므로 앨리어싱 불가)
 * - MIPS VIVT: 전체 D-cache flush (가장 비싼 연산)
 */

flush_dcache_page 구현

/* arch/arm/mm/flush.c — ARM32 VIPT 캐시 flush */
void flush_dcache_page(struct page *page)
{
    struct address_space *mapping;

    /* 익명 페이지: 단일 매핑이면 flush 불필요 */
    if (!PageAnon(page))
        goto flush;

    /* 이미 D-cache에 없는 페이지(cold)는 skip */
    if (!page_mapping_file(page))
        return;

flush:
    /* 커널 직접 매핑(lowmem)의 VA로 D-cache clean */
    __flush_dcache_page(mapping, page);

    /* user space 매핑들에 대해 해당 페이지의
     * 캐시 라인을 모두 invalidate */
    if (mapping && mapping_mapped(mapping))
        __flush_dcache_aliases(mapping, page);
}

/* ARM64(PIPT)에서는 flush_dcache_page가 훨씬 간단:
 * VA→PA 변환 불일치가 없으므로 앨리어싱 자체가 불가능.
 * DMA 동기화 목적으로만 clean/invalidate 수행. */
실전 앨리어싱 버그: 공유 메모리(shmem), mmap된 파일, copy-on-write 후 페이지가 여러 프로세스에서 다른 VA로 매핑될 때 앨리어싱이 발생합니다. 증상은 데이터 corruption으로 나타나며, 간헐적으로 발생하여 디버깅(Debugging)이 매우 어렵습니다. VIPT 캐시를 사용하는 SoC에서 신규 드라이버 개발 시 반드시 flush_dcache_page() 호출 여부를 점검하세요.

캐시 운영 진단 플레이북

캐시 관련 성능 문제를 체계적으로 진단하기 위한 단계별 가이드입니다. 증상에 따라 적절한 도구와 해결 방법을 선택합니다.

캐시 운영 진단 플레이북 — perf stat 으로 L1D · LLC · TLB 미스율을 한 번에 재고, 나쁜 카운터 쪽만 파고든다. L1D 는 코히런스 트래픽(HITM)이 있으면 perf c2c 로 어느 오프셋이 공유되는지 확인해 true sharing 과 false sharing 을 나누고, 없으면 용량·충돌 문제로 본다. 판정의 기준선은 절대 수치가 아니라 내 워크로드의 정상 상태를 미리 기록한 값이다. 캐시 운영 진단 플레이북 — 증상에서 조치까지 perf stat 한 번으로 출발하고, 나쁜 카운터가 있는 쪽만 파고든다 — 다른 쪽은 건드리지 않는다 1 출발 — perf stat 으로 세 가지를 한 번에 잰다 perf stat -e … 로 아래 세 쌍을 한 번에 측정한다 (비교 대상 baseline 을 먼저 기록해 둔다) L1-dcache-loads L1-dcache-load-misses LLC-loads LLC-load-misses dTLB · iTLB loads dTLB · iTLB misses 판정 기준은 절대 수치가 아니다 — 정상 상태의 미스율을 한 번 찍어 둔 baseline 과 비교해 "내 워크로드에서 유난히 나쁜 카운터" 를 고른다 2 증상 3갈래 — 나쁜 카운터의 자리를 따라간다 ① L1D 미스율 L1-dcache-loads / -misses 코히런스 트래픽(HITM)이 있는가? perf c2c record 로 줄 단위로 확인 Yes · perf c2c 로 좁힌다 같은 변수를 CPU 가 공유 → true sharing → 락 세분화 · per-CPU 배열 다른 변수가 한 라인 → false sharing → 캐시 라인 정렬로 분리 No · 코히런스 문제가 아니다 용량 / 충돌 문제로 본다 → 구조체 패딩으로 필드 분리 → 작업 집합 축소 · 순회 방향 변경 ② LLC 미스율 LLC-loads / -misses 원격 NUMA 노드 접근이 많은가? numastat · perf c2c 의 노드 정보 Yes · numactl 로 바인딩 CPU 고정 → taskset 메모리 고정 → mbind · set_mempolicy 로컬 LLC 를 가까운 노드에서 No · 용량 · 배치 문제 huge page 로 TLB 압박 완화 프리페치 · RDT CAT 로 파티셔닝 → CLOSID 별 용량 분할 ③ TLB 미스율 dTLB-loads · iTLB-loads dTLB 와 iTLB 중 어느 쪽이 큰가? 두 카운터를 따로 비교한다 dTLB · 데이터 쪽이 큼 huge page 적용 THP · hugetlbfs 변환 횟수 자체를 줄인다 iTLB · 코드 쪽이 큼 코드 텍스트 크기 축소 -Os · LTO · PGO 명령어 주소 범위를 좁힌다 HITM 은 "이 캐시 라인이 경합을 겪고 있다" 는 신호일 뿐이다 — true sharing 인지 false sharing 인지는 perf c2c 가 보여주는 오프셋을 직접 확인해야 갈린다 3 위의 어느 갈래를 타든 공통으로 볼 항목 데이터 구조 hot/cold 분리 · 캐시 라인 정렬 · 배열 순회 방향 · 구조체 패딩 시스템 레벨 NUMA 바인딩 · huge page · RDT CAT/MBA · irqbalance · CPU pinning

캐시 미스 종합 진단

#!/bin/bash
# cache_diagnosis.sh — 캐시 성능 종합 진단 스크립트

TARGET=$1
if [ -z "$TARGET" ]; then
    echo "Usage: $0 <command>"
    exit 1
fi

echo "=== Phase 1: 기본 캐시 통계 ==="
perf stat -e \
  cache-references,cache-misses,\
  L1-dcache-loads,L1-dcache-load-misses,\
  L1-icache-load-misses,\
  LLC-loads,LLC-load-misses,\
  dTLB-loads,dTLB-load-misses,\
  iTLB-loads,iTLB-load-misses \
  -- $TARGET 2>&1 | tee /tmp/cache_phase1.txt

echo "=== Phase 2: 메모리 접근 지연 분포 ==="
perf mem record -t load -- $TARGET
perf mem report --sort=mem --stdio | head -30

echo "=== Phase 3: Cache-to-Cache 분석 ==="
perf c2c record -- $TARGET
perf c2c report --stdio --stats | head -50

echo "=== 결과 요약 ==="
L1_MISS=$(grep "L1-dcache-load-misses" /tmp/cache_phase1.txt | awk '{print $NF}')
echo "L1D miss rate: $L1_MISS"
LLC_MISS=$(grep "LLC-load-misses" /tmp/cache_phase1.txt | awk '{print $NF}')
echo "LLC miss rate: $LLC_MISS"

False Sharing 탐지 절차

# perf c2c로 false sharing 핫스팟 식별
perf c2c record -a -g -- sleep 10
perf c2c report --stdio -d lcl

# 출력 해석:
# Shared Data Cache Line Table
# Total     Rmt   Lcl  Tot   Ld    St
# records   hitm  hitm hitm  miss  miss  Symbol
# -------   ----  ---- ----  ----  ----  ------
#  15234     423   156  579   234   345   my_global_struct+0x40
#
# → my_global_struct의 오프셋 0x40에서 심각한 false sharing
# → 구조체 내 해당 필드를 별도 캐시 라인으로 분리

# pahole로 구조체 레이아웃 확인
pahole -C my_global_struct ./my_program

# 예상 출력:
# struct my_global_struct {
#     u64    reader_count;        /* 0     8 */
#     u64    writer_count;        /* 8     8 */  ← 같은 캐시 라인!
#     /* ... */
# };
#
# 해결: __cacheline_aligned 삽입
# struct my_global_struct {
#     u64    reader_count;
#     u64    writer_count __cacheline_aligned;  ← 분리!
# };

NUMA 캐시 최적화

# NUMA 원격 캐시 접근 비율 확인
perf stat -e \
  node-loads,node-load-misses,\
  node-stores,node-store-misses \
  -- ./workload

# numastat으로 NUMA 밸런스 확인
numastat -p $(pidof workload)

# 원격 접근 비율이 높으면:
# 1. 프로세스를 로컬 노드에 바인딩
numactl --cpunodebind=0 --membind=0 ./workload

# 2. 커널의 자동 NUMA 밸런싱 활성화
echo 1 > /proc/sys/kernel/numa_balancing

# 3. perf로 NUMA 마이그레이션 이벤트 추적
perf stat -e migrate:mm_migrate_pages -- sleep 10

cachestat() 시스콜 (Linux 6.5+)

/* Linux 6.5에서 추가된 cachestat() 시스콜
 * — 파일의 페이지 캐시(Page Cache) 상태를 효율적으로 조회 */
#include <linux/cachestat.h>

struct cachestat_range range = {
    .off = 0,
    .len = file_size,
};
struct cachestat cs;

/* 파일의 페이지 캐시 통계 조회 */
int ret = syscall(__NR_cachestat, fd, &range, &cs, 0);

printf("Cached pages:     %llu\n", cs.nr_cache);
printf("Dirty pages:      %llu\n", cs.nr_dirty);
printf("Writeback pages:  %llu\n", cs.nr_writeback);
printf("Evicted pages:    %llu\n", cs.nr_evicted);
printf("Recently evicted: %llu\n", cs.nr_recently_evicted);

/* mincore()보다 한 번의 시스콜로 여러 카운터를 얻습니다:
 * - 범위 내 캐시된 페이지 수(nr_cache)를 한 번에 조회
 * - dirty / writeback / evicted 상태를 함께 판별
 * - 데이터베이스 버퍼 풀 관리에 유용
 * - 페이지를 다시 채우기 전에 사전 확인하는 용도로 활용
 */
진단 우선순위: 캐시 문제 진단은 항상 perf stat → 병목(Bottleneck) 식별 → 해당 도구 분석 순서로 진행하세요. 가장 흔한 성능 개선 순서는: (1) false sharing 제거 (2) NUMA 바인딩 (3) 데이터 구조 정렬 (4) 프리페칭 (5) RDT 파티셔닝입니다.

resctrl 확장 및 아키텍처 개선 (v6.14~v6.15)

총 메모리 대역폭 모니터링 이벤트 (Linux 6.14)

Linux 6.14에서 resctrl의 BMEC(Bandwidth Monitoring Event Configuration) 지원이 확장되어, 도메인별 로컬 대역폭(mbm_local_bytes)에 더해 총 메모리 대역폭 이벤트(mbm_total_bytes)를 시스템 전체 단위로 설정할 수 있게 되었습니다. 일부 플랫폼은 도메인별 세분화 대신 총량 모니터링만 지원하며, 멀티-NUMA 워크로드에서 전체 대역폭 압박을 단일 수치로 파악하는 데 유용합니다.

커널 6.14부터: Intel RDT/AMD QoS resctrl에서 mbm_total_bytes 이벤트 필터 설정이 가능합니다. 기존 mbm_total_bytes 읽기 경로는 그대로 유지되며, BMEC 지원 여부에 따라 설정 노브가 추가됩니다.

resctrl 코드 아키텍처 중립 위치로 이동 (Linux 6.16)

Linux 6.16에서 resctrl의 공통 코어가 arch/x86/kernel/cpu/resctrl/에서 fs/resctrl/로 이동했습니다. fs/resctrl/에는 rdtgroup.c, ctrlmondata.c, monitor.c, pseudo_lock.c 등 사용자 인터페이스 계층이 모이고, arch/x86/kernel/cpu/resctrl/에는 core.c 등 x86 하드웨어 구현이 남습니다. 이후 ARM64 MPAM이 drivers/resctrl/(mpam_devices.c, mpam_resctrl.c)로 들어와 ARCH_HAS_CPU_RESCTRL를 arm64에서도 선택하게 되면서, 두 아키텍처가 동일한 사용자 인터페이스를 공유합니다.

AMD INVLPGB 브로드캐스트 TLB 무효화 (Linux 6.15)

Linux 6.15에서 AMD Zen 3 이상 프로세서의 INVLPGB(INVaLidate Page Global Broadcast) 명령어 지원이 추가되었습니다. 기존에는 TLB 항목을 무효화할 때 원격 CPU에 IPI(프로세서 간 인터럽트(Interrupt))를 전송해야 했으나, INVLPGB를 사용하면 하드웨어가 모든 코어에 동시에 TLB 무효화를 브로드캐스트합니다.

캐시 QoS 최신 동향 — AMD ABMC · ARM64 MPAM (2025-2026)

2025~2026년 기간에 캐시/메모리 대역폭 QoS 분야는 두 축에서 크게 확장되었습니다. Intel은 기존 RDT/CAT을 정제했고, AMD는 EPYC에서 ABMC를 도입하여 기존 bandwidth monitoring의 한계를 돌파했으며, ARM64는 MPAM 드라이버를 메인라인에 편성해 Intel resctrl과 동일한 사용자 인터페이스를 쓰게 되었습니다.

AMD ABMC (Linux 6.18 LTS)

ABMC(Assignable Bandwidth Monitoring Counters)는 AMD EPYC에서 메모리 대역폭 모니터링 카운터를 리소스 그룹에 명시적으로 할당할 수 있게 한 확장입니다. 기존 AMD MBM은 여러 리소스가 카운터를 공유해야 해서 대형 테넌트 환경에서 정확도가 떨어졌는데, ABMC로 이를 해결합니다.

# ABMC (mbm_event) 카운터 할당 모드 활성화 — Linux v6.18 문서 기준
# 1) 하드웨어 카운터 수와 사용 가능한(미할당) 카운터 수 확인
cat /sys/fs/resctrl/info/L3_MON/num_mbm_cntrs          # 예: 0=32;1=32  (도메인별 총 카운터)
cat /sys/fs/resctrl/info/L3_MON/available_mbm_cntrs     # 예: 0=30;1=30  (할당 가능 카운터)

# 2) mbm_event 카운터 할당 모드로 전환
echo "mbm_event" > /sys/fs/resctrl/info/L3_MON/mbm_assign_mode
#    복귀: echo "default" > /sys/fs/resctrl/info/L3_MON/mbm_assign_mode

# 3) 그룹별로 카운터 할당 / 해제 ("_": 해제, "e": 배타 할당)
mkdir /sys/fs/resctrl/mon_group
echo $MON_PID > /sys/fs/resctrl/mon_group/tasks
cat /sys/fs/resctrl/mbm_L3_assignments                  # 예: mbm_total_bytes:0=_;1=_
echo "mbm_total_bytes:0=e" > /sys/fs/resctrl/mon_group/mbm_L3_assignments
echo "mbm_total_bytes:1=e" > /sys/fs/resctrl/mon_group/mbm_L3_assignments
cat /sys/fs/resctrl/mon_group/mon_data/mon_L3_00/mbm_total_bytes

# 해제: echo "mbm_total_bytes:0=_" > /sys/fs/resctrl/mon_group/mbm_L3_assignments
# 할당 전에는 MBM 이벤트가 'Unassigned' 로 표시됨

ARM64 MPAM (Linux 6.19)

MPAM(Memory Partitioning and Monitoring)은 ARMv8.4+에서 정의된 QoS 아키텍처로, 캐시 포션(portion)과 메모리 대역폭을 파티션 ID(PARTID)로 구분합니다. arm_mpam 디바이스 드라이버는 v6.19에 drivers/resctrl/mpam_devices.c 형태로 메인라인에 편성되었고, resctrl 사용자 인터페이스 연동(mpam_resctrl.c, ARCH_HAS_CPU_RESCTRL)은 그 이후 릴리스에서 추가되었습니다. Intel RDT/resctrl 대비 다음 특성이 있습니다.

특성Intel RDT/resctrlARM64 MPAM (v6.19+)
분할 단위CLOSID + RMIDPARTID + PMG
캐시 분할CAT — CLOSID별 CBM Way 비트마스크MPAM CPOR — L2/L3 캐시 포션 비트맵(Bitmap)
대역폭 제한MBA — 대역폭 비율(%) 제한MPAM MBW_MAX — 최대 대역폭 제한
모니터링CMT(llc_occupancy) / MBMMPAM CSU(캐시 점유) / MBWU(대역폭)
사용자 인터페이스/sys/fs/resctrl/sys/fs/resctrl (공통 인터페이스 재사용)
지원 제약—mbm_local_bytes 미노출 — MPAM은 로컬/원격 트래픽을 구분할 수 없음
적용 범위LCC 중심L3에 대응하는 MSC 그룹 (L3 캐시·메모리 컨트롤러), L2/L3 혼합 지정 불가
유니파이드 resctrl 방향: ARM64 MPAM 드라이버는 사용자 경로에서 resctrl 파일시스템을 그대로 따르도록 설계되어, x86과 ARM64 모두 동일한 툴체인(예: pqos, intel-cmt-cat의 포트)으로 운영할 수 있도록 수렴 중입니다.

참고자료

공식 규격 및 표준

커널 문서

LWN 기사

커널 소스 코드

컨퍼런스 발표 및 기술 자료

필수 관련 문서:
  • CPU 토폴로지(CPU Topology) — 코어·스레드·LLC 공유 관계와 NUMA 노드 경계를 정의합니다. 캐시 계층을 이해하려면 Topology가 먼저입니다
  • MMU & TLB — VIPT/PIPT 인덱싱 방식과 앨리어싱, TLB shootdown의 상세 동작을 다룹니다
  • NUMA — 원격 노드 LLC 접근 지연과 numactl/numastat 기반 캐시 친화 배치 기법을 다룹니다
  • 메모리(Memory) — Slab 할당자, 페이지 캐시(Page Cache), DMA 캐시 동기화(dma_sync_*) 경로를 다룹니다
  • 메모리 배리어(Memory Barrier) — 코히런시 프로토콜이 보장하는 메모리 가시성 순서와 mfence/sfence의 관계를 다룹니다
  • perf 서브시스템(perf) — cache-misses, perf c2c, perf mem 이벤트 해석 방법을 다룹니다