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, 실전 진단까지 종합적으로 다룹니다.
핵심 요약
- 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보다 약 4배 느립니다.
numactl --membind로 지역성을 강제하세요. - PMEM 영속성 — CLWB+SFENCE 없이는 전원 장애 시 캐시에 남아 있는 데이터가 소실될 수 있습니다. DAX 경로에서 반드시 필요합니다.
단계별 이해
- 캐시 확인 —
lscpu또는getconf -a | grep CACHE로 시스템의 캐시 크기와 라인 크기를 확인합니다./sys/devices/system/cpu/cpu0/cache/에서 상세 정보를 볼 수 있습니다. - 적중/미스 이해 — 데이터가 캐시에 있으면 Hit(수 ns), 없으면 Miss(수십~수백 ns)입니다.
perf stat -e cache-misses,cache-references ./app으로 캐시 효율을 측정합니다. - 코히런시 이해 — 코어 A가 데이터를 수정하면, 코어 B의 같은 캐시 라인이 무효화(Invalidate)됩니다.
이것이 MESI 프로토콜의 핵심이며, 멀티스레드 프로그램 성능에 큰 영향을 줍니다.
- 최적화 기법 — 데이터를 캐시 라인에 정렬하고, False Sharing을 피하면 성능이 크게 향상됩니다.
커널에서는
____cacheline_aligned매크로(Macro)로 구조체(Struct) 필드를 정렬합니다. - NUMA 영향 확인 —
numastat -c,perf stat -e LLC-load-misses,node-load-misses로 NUMA 캐시 효율을 점검합니다.원격 노드 LLC 미스가 많으면
numactl --membind=0이나taskset으로 NUMA 지역성을 강제하세요. - 캐시 인덱싱 확인 —
/sys/devices/system/cpu/cpu0/cache/index0/physical_line_partition으로 L1D 캐시의 인덱싱 방식을 확인합니다.cat /sys/devices/system/cpu/cpu0/cache/index0/type은 "Data"를 반환합니다. ARM 시스템에서는 VIPT 여부를 커널 로그에서도 확인할 수 있습니다. - eBPF 분석 —
bpftrace -e 'hardware:cache-misses:1000 { @[comm] = count(); }'로 프로세스별 캐시 미스를 분석합니다.llcstat-bpfcc명령으로 코어/프로세스별 LLC 히트율 통계를 실시간(Real-time) 확인하세요. - PMEM 영속 패턴 — DAX 기반 영속 스토리지 코드에서
clwb addr후 반드시sfence를 실행하여 영속성을 보장합니다.커널의
arch_wb_cache_pmem()과pmem_flush()는 이 시퀀스를 올바르게 구현합니다.
캐시 기본 원리
캐시 라인
캐시의 최소 전송 단위는 캐시 라인(cache line)으로, 현대 x86/ARM 프로세서에서 대부분 64바이트입니다. 메모리 주소를 캐시 라인 크기로 나눈 몫이 같은 바이트들은 항상 함께 캐시에 올라옵니다. 따라서 구조체의 핫 필드를 같은 캐시 라인에 배치하면 하나의 캐시 미스로 여러 필드를 동시에 읽을 수 있습니다.
/* 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;
};
공간적 / 시간적 지역성
캐시가 효과적인 이유는 프로그램의 지역성(locality) 때문입니다.
- 시간적 지역성(Temporal Locality): 최근 접근한 데이터는 곧 다시 접근될 가능성이 높습니다. 예: slab 캐시에서 자주 할당/해제되는 객체.
- 공간적 지역성(Spatial Locality): 접근한 주소 근처의 데이터도 곧 접근될 가능성이 높습니다. 예: 페이지 테이블 워크에서 연속된 PTE 읽기.
히트와 미스
캐시 히트(hit)는 요청한 데이터가 캐시에 존재하는 경우이고, 미스(miss)는 존재하지 않아 하위 메모리 계층에서 가져와야 하는 경우입니다. 미스는 세 가지로 분류됩니다:
- Compulsory (cold) miss: 데이터를 처음 접근할 때. 프리페칭으로 완화 가능.
- Capacity miss: 캐시 용량이 부족하여 이전 데이터가 축출(Eviction). 더 큰 캐시나 작업 집합(working set) 축소로 완화.
- Conflict miss: 연관도(associativity)가 부족하여 같은 셋에 매핑(Mapping)된 라인끼리 충돌. 더 높은 연관도로 완화.
cold 페이지는 캐시에 없을 가능성이 높은 페이지, hot 페이지는 캐시에 있을 가능성이 높은 페이지를 의미합니다. 페이지 할당자(Page Allocator)의 per-CPU 리스트는 hot/cold 페이지를 구분하여 캐시 효율을 높입니다.
캐시 계층 구조
L1 캐시
L1 캐시는 CPU 코어에 가장 가까운 캐시로, 명령어 캐시(L1I)와 데이터 캐시(L1D)로 분리(Harvard 구조)되어 있습니다. 접근 지연(Latency)은 약 4~5 사이클이며, 코어당 독립적으로 존재합니다.
L2 캐시
L2 캐시는 명령어와 데이터를 통합(unified)하여 저장하며, 접근 지연은 약 12~14 사이클입니다. 대부분의 현대 프로세서에서 코어별 또는 코어 클러스터별로 할당됩니다.
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만 확인하면 되어 코히런시 간단. | Intel Broadwell 이전 |
| Exclusive | 각 레벨에 데이터가 한 곳에만 존재. 총 유효 용량이 L1+L2+L3. | AMD Zen1~Zen3 |
| NINE | Non-Inclusive Non-Exclusive. L3 축출이 L2를 무효화하지 않음. | Intel Skylake-SP+, AMD Zen4+ |
ARM 캐시 계층
ARM 프로세서는 제조사와 설계에 따라 캐시 구조가 다양하지만, 고성능 코어(Cortex-A/Neoverse)는 일반적으로 다음 계층을 따릅니다:
| 레벨 | Cortex-X4 예시 | Neoverse V2 예시 | 특징 |
|---|---|---|---|
| L1I | 64KB (4-way) | 64KB (4-way) | 코어별 독립 |
| L1D | 64KB (4-way) | 64KB (4-way) | 코어별 독립, VIPT |
| L2 | 1MB (8-way) | 2MB (8-way) | 코어별 독립, PIPT |
| L3 (SLC) | 8MB (16-way) | 32MB (16-way) | DSU 공유, 슬라이스 분산 |
- DSU (DynamIQ Shared Unit): Cortex-A/X 혼합 클러스터의 L3 캐시와 인터커넥트. 최대 8개 코어가 공유.
- SLC (System Level Cache): ARM의 L3 명칭. 슬라이스 방식으로 분산 배치.
- CMN (Coherent Mesh Network): Neoverse 서버급 프로세서의 메시 인터커넥트. 여러 DSU를 연결.
- VIPT vs PIPT: L1D는 VIPT (Virtually-Indexed Physically-Tagged), L2/L3는 PIPT (Physically-Indexed Physically-Tagged)로 캐시 앨리어싱 회피.
캐시 연관도
직접 사상 (Direct-Mapped)
각 메모리 블록이 캐시의 정확히 한 위치에만 매핑됩니다. 구현이 간단하고 접근이 빠르지만 conflict miss가 빈번합니다. 현대 프로세서에서는 거의 사용되지 않습니다.
N-Way 집합 연관 (Set-Associative)
현대 캐시의 표준 방식입니다. 캐시를 여러 셋(set)으로 나누고, 각 셋에 N개의 웨이(way)를 둡니다. 메모리 주소는 하나의 셋에 매핑되지만, 그 셋 내 N개 웨이 중 아무 곳에나 저장될 수 있습니다.
셋 인덱스 계산:
셋 수 = 캐시 크기 / (캐시 라인 크기 × 연관도)
셋 인덱스 = (주소 / 캐시 라인 크기) % 셋 수
완전 연관 (Fully-Associative)
메모리 블록이 캐시의 어느 위치에나 저장될 수 있습니다. Conflict miss가 없지만 검색 비용이 높아 TLB 같은 소규모 캐시에 주로 사용됩니다.
태그 / 셋 / 오프셋(Offset) 비트 분해
물리 주소(Physical Address)는 세 영역으로 분해됩니다:
| 필드 | 비트 수 (예: 32KB 8-way, 64B line) | 용도 |
|---|---|---|
| Offset | 6 (log2(64)) | 캐시 라인 내 바이트 위치 |
| Set Index | 6 (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;
}
/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를 기다리지 않아 가장 빠르지만, 두 가지 심각한 문제가 있습니다:
- 컨텍스트 스위치 시 전체 플러시: 다른 프로세스(Process)가 같은 가상 주소를 다른 물리 주소로 사용할 수 있어, 스위치마다 캐시를 완전히 비워야 합니다.
- Homonym 문제: 같은 가상 주소가 다른 물리 주소를 가리킬 때 캐시가 잘못된 데이터를 반환합니다.
초기 ARM 프로세서(ARM926 등)에서 사용됐으나 현대 설계에서는 거의 사용하지 않습니다.
VIPT (Virtually-Indexed Physically-Tagged)
인덱스는 가상 주소, 태그는 물리 주소로 계산합니다. TLB와 캐시 접근을 병렬로 시작하여 성능을 유지하면서 Homonym 문제를 해결합니다. x86 L1D와 ARM Cortex-A/X L1D가 이 방식을 사용합니다.
앨리어싱 회피 조건:
인덱스 비트 수 ≤ page_offset_bits (= log2(페이지 크기))
예: 페이지 크기 4KB (12비트), 캐시 라인 64B (6비트)
셋 수 = 캐시 크기 / (라인 크기 × 웨이 수)
인덱스 비트 = log2(셋 수) = log2(캐시 크기 / (64 × N))
32KB 8-way: 셋 = 32768 / (64×8) = 64 → 인덱스 6비트 ≤ 12비트 → VIPT = PIPT (앨리어싱 없음)
64KB 4-way: 셋 = 65536 / (64×4) = 256 → 인덱스 8비트 > 12비트 → 앨리어싱 가능!
인덱스 비트가 페이지 오프셋 비트보다 많으면 캐시 앨리어싱(aliasing)이 발생합니다. 같은 물리 페이지를 서로 다른 가상 주소로 매핑할 때, 인덱스가 달라져 캐시에 동일 데이터의 복사본이 두 곳에 생기는 문제입니다.
PIPT (Physically-Indexed Physically-Tagged)
인덱스와 태그 모두 물리 주소로 계산합니다. TLB 변환이 완료된 후 캐시를 접근하므로 앨리어싱 문제가 없습니다. ARM L2/L3(Cortex-A, Neoverse), x86 L2/L3가 사용합니다. 변환 대기 지연이 있지만, L2/L3는 L1보다 지연이 크므로 이 오버헤드(Overhead)가 상대적으로 작습니다.
커널에서의 앨리어싱 처리
VIPT 캐시에서 앨리어싱이 발생하는 ARM 아키텍처는 다음 함수로 관리합니다:
/* arch/arm/mm/cache-v7.c — VIPT 앨리어싱 캐시에서 페이지 플러시 */
void flush_cache_page(struct vm_area_struct *vma,
unsigned long user_addr, unsigned long pfn)
{
unsigned long addr = pfn_to_kaddr(pfn);
/* VIPT 앨리어싱: 사용자 VA + 커널 VA 양쪽 모두 플러시 */
__flush_dcache_page(pfn, vma);
}
/* include/asm-generic/cacheflush.h */
void flush_dcache_page(struct page *page);
/* DMA/mmap 후 물리 페이지 캐시 일관성 보장 — VIPT 아키텍처에서 필수 */
/* ARM64는 PIPT L1D를 사용하므로 flush_cache_page()가 NOP (no-op) */
/* ARM32 VIPT 캐시에서는 실제 캐시 라인 플러시 수행 */
| 인덱싱 방식 | 인덱스 | 태그 | TLB 대기 | 앨리어싱 | 주요 사용처 |
|---|---|---|---|---|---|
| VIVT | 가상 | 가상 | 불필요 | 컨텍스트 스위치마다 플러시 | 초기 ARM (ARM926) |
| VIPT | 가상 | 물리 | 병렬 (Tag만) | 인덱스 비트 > page offset이면 발생 | x86 L1D, ARM L1D |
| PIPT | 물리 | 물리 | 필요 (직렬) | 없음 | x86/ARM L2/L3 |
flush_dcache_page()는 대부분의 경우 no-op이지만, ARM32나 커스텀 임베디드 코어에서는 중요합니다.
캐시 교체 정책
LRU / Pseudo-LRU
캐시 셋이 가득 찼을 때 어떤 라인을 축출할지 결정하는 정책입니다.
- LRU (Least Recently Used): 가장 오래 사용되지 않은 라인을 축출. 이상적이지만 높은 연관도에서 상태 추적 비용이 큼.
- Pseudo-LRU (PLRU): 트리 기반의 근사 LRU. 8-way 셋에서 3비트 트리로 축출 후보를 O(1)에 결정. 현대 x86 프로세서에서 가장 널리 사용.
적응형 교체
- Intel Adaptive Replacement: Skylake 이후 LLC에서 접근 빈도와 최근성을 모두 고려하는 적응형 정책 사용.
- AMD: L3에서 워크로드에 따라 동적으로 교체 정책을 조정.
- ARM: 일부 구현에서 Random 교체를 사용하여 병적(pathological) 패턴 방지.
RRIP / SHIP — 현대 캐시 교체 알고리즘
Intel Haswell 이후의 LLC는 단순 LRU/PLRU 대신 더 정교한 교체 알고리즘을 사용합니다:
- RRIP (Re-Reference Interval Prediction): 각 캐시 라인에 2비트의 RRPV(Re-Reference Prediction Value)를 유지합니다. RRPV가 높을수록 곧 재접근될 가능성이 낮아 교체 우선 대상이 됩니다. 새 라인 삽입 시 RRPV=2(far)로 시작하여, 캐시 히트마다 RRPV를 0(near)으로 낮춥니다. LRU보다 스캔(scan) 워크로드에 더 강합니다.
- SHIP (Signature-based Hit Predictor): PC(Program Counter) 기반 접근 패턴을 시그니처 테이블(SHCT)에 기록하여 이전에 캐시 히트를 유발한 명령어인지 예측합니다. 히트 예측이 높으면 RRPV=0으로 삽입하여 캐시에 오래 유지합니다.
| 알고리즘 | 히트율 (SPEC CPU 기준) | 상태 비트 | 스캔 저항성 | 사용 사례 |
|---|---|---|---|---|
| LRU | 기준 (1.0×) | log2(N) × way | 낮음 | 소규모 캐시 |
| Pseudo-LRU (PLRU) | ~0.98× | N-1 비트/셋 | 낮음 | x86 L1/L2 (대부분) |
| RRIP | ~1.05× | 2비트/way | 높음 | Intel L3 (Haswell+) |
| SHIP | ~1.08× | RRIP + SHCT | 높음 | Intel LLC (Broadwell+) |
쓰기 정책
Write-Back
대부분의 현대 프로세서가 기본으로 사용하는 정책입니다. 쓰기 시 캐시만 갱신하고, 더티(dirty) 비트를 설정합니다. 캐시 라인이 축출될 때만 메모리에 기록하므로 메모리 대역폭(Bandwidth)을 절약합니다.
Write-Through
쓰기 시 캐시와 메모리를 동시에 갱신합니다. 코히런시 관리가 간단하지만 쓰기 대역폭을 많이 소모합니다. 일부 임베디드 시스템이나 특수 용도에서 사용됩니다.
Write-Allocate / No-Write-Allocate
- Write-Allocate (Fetch on Write): 쓰기 미스 시 캐시 라인을 먼저 로드한 후 쓰기. Write-Back과 조합이 일반적.
- No-Write-Allocate: 쓰기 미스 시 메모리에 직접 기록하고 캐시에 올리지 않음. Write-Through와 조합.
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);
}
ioremap_wc()와 set_memory_wc()를 혼용하면 PAT(Page Attribute Table) 엔트리가 충돌할 수 있습니다. 항상 매핑 해제 후 새 타입으로 재매핑하세요.
캐시 코히런시 프로토콜
멀티코어 시스템에서 여러 코어의 캐시가 동일 메모리 주소의 다른 값을 가지면 안 됩니다. 캐시 코히런시 프로토콜은 각 캐시 라인의 상태를 추적하여 일관성을 보장합니다.
MESI 프로토콜
가장 기본적인 코히런시 프로토콜로, 각 캐시 라인은 네 가지 상태 중 하나입니다:
- Modified (M): 이 코어만 가지고 있으며, 메모리보다 새로운 값 (dirty).
- Exclusive (E): 이 코어만 가지고 있으며, 메모리와 같은 값 (clean).
- Shared (S): 여러 코어가 가지고 있으며, 메모리와 같은 값.
- Invalid (I): 유효하지 않은 라인 (빈 슬롯과 동일).
MOESI (AMD)
AMD는 MESI에 Owned (O) 상태를 추가한 MOESI 프로토콜을 사용합니다. Owned 상태의 코어는 다른 코어들과 데이터를 공유하면서도 수정된 값의 책임을 집니다 — 메모리에 쓰기를 지연시키면서 스누프 요청에 직접 응답할 수 있어, Modified→Shared 전환 시 불필요한 메모리 쓰기를 회피합니다.
MESIF (Intel)
Intel은 MESI에 Forward (F) 상태를 추가한 MESIF 프로토콜을 사용합니다. Shared 상태의 여러 코어 중 하나가 Forward로 지정되어 스누프 요청에 응답하는 역할을 합니다. 이를 통해 Shared 상태에서 여러 코어가 동시에 응답하는 중복을 방지합니다.
스누핑 vs 디렉토리 기반
| 메커니즘 | 원리 | 확장성 | 구현 예 |
|---|---|---|---|
| 스누핑 | 모든 코어가 버스 트래픽을 감시(snoop) | 낮음 (~8코어) | 초기 SMP |
| Snoop Filter | LLC에 태그 디렉토리 유지, 불필요한 스누프 억제 | 중간 | Intel Skylake-SP+ |
| Probe Filter | AMD의 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 구성 요소
- RN-F (Request Node - Fully coherent): CPU 클러스터. 코히런트 요청 발행.
- HN-F (Home Node - Fully coherent): 메모리 컨트롤러. 디렉토리 유지, 스누프 조정.
- SN-F (Slave Node - Fully coherent): L3/SLC 슬라이스. 캐시 데이터 저장.
- RN-I/RN-D: 비코히런트 I/O 디바이스.
CHI 캐시 상태 (MOESI 확장)
CHI는 MOESI를 확장한 7개 상태를 사용합니다:
| 상태 | 의미 | MESI 대응 |
|---|---|---|
| I (Invalid) | 무효 | Invalid |
| UC (Unique Clean) | 유일 복사본, clean | Exclusive |
| UD (Unique Dirty) | 유일 복사본, dirty | Modified |
| SC (Shared Clean) | 공유, clean | Shared |
| SD (Shared Dirty) | 공유, dirty (다른 노드가 최신값 보유) | - |
| UDP (Unique Dirty Partial) | 부분 갱신, dirty | - |
| UCE (Unique Clean Empty) | 할당됐으나 데이터 없음 | - |
CHI 장점
- 확장성: 디렉토리 기반으로 브로드캐스트 스누프 불필요. CMN 메시로 수백 코어 연결 가능.
- 대역폭 효율: 스누프 트래픽이 관련 노드에만 전송. 불필요한 브로드캐스트 제거.
- QoS 지원: 트랜잭션별 우선순위(Priority)로 latency-critical 워크로드 보호.
- DVM (Distributed Virtual Memory): TLB/캐시 유지보수 작업을 분산 처리.
TLBI명령어가 CHI를 통해 전파.
arch/arm64/mm/cache.S에서 CHI의 캐시 유지보수 명령어를 활용합니다.
smp_wmb()/smp_rmb() 같은 메모리 배리어(Memory Barrier)는 코히런시 프로토콜이 전파를 완료하기 전에 다른 코어가 순서가 뒤바뀐 값을 관찰하는 것을 방지합니다.
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: 8 | 8-way |
| L2 STLB | 통합 | 4K+2M: 2048 | 16-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 reach | dTLB 미스율 | 실행 시간 | 성능 향상 |
|---|---|---|---|---|
| 4KB (기본) | 8MB | 12.4% | 8.5초 | 기준 |
| 2MB (Huge) | 4GB | 0.3% | 3.2초 | 2.7× |
| 1GB (Gigantic) | 2TB | <0.01% | 2.9초 | 2.9× |
# 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
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비트 태그 */
munmap()이나 메모리 해제 시 빈번히 발생하므로, 지나치게 잦은 VMA 조작은 성능 저하의 원인이 됩니다. PCID/ASID를 활용하면 전체 TLB 플러시 대신 선택적 무효화가 가능합니다.
캐시 프리페칭
하드웨어 프리페처
현대 프로세서는 메모리 접근 패턴을 감지하여 자동으로 데이터를 미리 캐시에 로드합니다:
- Stride prefetcher: 일정한 간격(stride)의 접근 패턴을 감지. 배열 순회에 효과적.
- Stream prefetcher: 연속 주소 접근을 감지하여 다음 캐시 라인을 미리 로드.
- Spatial prefetcher: 캐시 라인 쌍(pair)을 함께 로드. Intel L2 Adjacent Cache Line Prefetcher.
- Intel L2 Streamer: L2 미스를 모니터링하여 최대 20 캐시 라인 앞까지 프리페치.
소프트웨어 프리페치
커널은 명시적 프리페치 명령으로 하드웨어 프리페처를 보완합니다:
/* 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 | 동작 |
|---|---|---|
| PREFETCHT0 | 3 | L1 + L2 + L3로 프리페치 |
| PREFETCHT1 | 2 | L2 + L3로 프리페치 |
| PREFETCHT2 | 1 | L3로 프리페치 |
| PREFETCHNTA | 0 | 비시간적(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);
}
}
perf stat으로 효과를 측정한 후 유지 여부를 결정하세요.
AMD Zen4 프리페처와 CLDEMOTE
AMD Zen4 이후의 프리페처와 Intel Tiger Lake 이후의 새 캐시 명령어입니다:
- AMD MFMA 프리페처 (Zen4): AI/머신러닝 워크로드의 행렬 접근 패턴(
_mm512_loadu_ps등)을 감지하여 MFMA 명령어 실행 전 데이터를 미리 L2로 로드합니다. 대규모 행렬 곱(GEMM)에서 LLC 미스를 크게 줄입니다. - PREFETCHRST2: AMD Zen4의 확장 프리페치 명령어.
PREFETCHT2와 유사하지만 실패 시 재시작(restart) 시 성능 패널티를 줄이도록 최적화되었습니다.
/* 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 공통 |
| CLDEMOTE | L1 → L2/L3 | 데이터를 하위 레벨로 내림 | Intel Tiger Lake+ |
| AMD PREFETCHRST2 | 메모리 → L3 | 실패 시 재시작 패널티 감소 | AMD Zen4+ |
캐시 파티셔닝 — 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) |
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/s | LLC 미스 시 |
| 원격 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);
}'
taskset)하더라도 메모리가 다른 노드에 할당되면 원격 LLC/DRAM 접근이 발생합니다. numactl --cpunodebind=N --membind=N으로 코어와 메모리를 같은 노드에 함께 바인딩하세요.
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에서 문제 변수와 소스 위치 표시
완화
__cacheline_aligned_in_smp: SMP 빌드에서만 캐시 라인 정렬 (UP에서는 낭비 방지).- per-CPU 변수:
DEFINE_PER_CPU()로 코어마다 독립 복사본을 유지하면 공유 자체가 없음. - pahole: 구조체의 캐시 라인 레이아웃을 시각화하여 false sharing 후보를 탐지.
# pahole로 구조체 레이아웃 확인
pahole --class_name task_struct vmlinux | head -50
# 각 필드의 오프셋과 캐시 라인 경계를 표시
perf stat에서 L1-dcache-load-misses가 비정상적으로 높고 perf c2c에서 HITM이 집중되는 캐시 라인이 있다면 false sharing을 의심하세요.
성능 영향 측정
실제 벤치마크 결과로 false sharing의 성능 저하를 확인할 수 있습니다:
| 구성 | 처리량 (ops/sec) | L1 미스율 | HITM 비율 | 배수 |
|---|---|---|---|---|
| False Sharing (같은 라인) | 12M | 45% | 38% | 1.0× |
| 패딩(Padding) 적용 (별도 라인) | 89M | 8% | <1% | 7.4× |
| per-CPU 변수 | 156M | 2% | 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
- WBINVD: CPU의 전체 캐시를 메모리에 기록 후 무효화. 매우 느리고 모든 코어에 영향. 리셋, 절전 진입 시 사용.
- INVD: 전체 캐시를 기록 없이 무효화. 데이터 손실 위험. BIOS/펌웨어(Firmware) 전용.
Non-Temporal 스토어
Non-Temporal 스토어(MOVNTI, MOVNTPS 등)는 캐시를 우회하여 메모리에 직접 기록합니다. 대량 데이터 복사 시 캐시 오염을 방지합니다.
| 명령어 | 동작 | 캐시 무효화 | 순서 보장(Ordering) | 용도 |
|---|---|---|---|---|
| CLFLUSH | Write-back + Invalidate | O | 직렬화(Serialization) | 일반 캐시 플러시 |
| CLFLUSHOPT | Write-back + Invalidate | O | 느슨 (SFENCE 필요) | 병렬 플러시 |
| CLWB | Write-back only | X (힌트) | 느슨 (SFENCE 필요) | PMEM 영속 |
| WBINVD | 전체 Write-back + Invalidate | O (전체) | 직렬화 | 리셋, S3 진입 |
| MOVNTI | WC 버퍼 경유 스토어 | 해당 없음 | 느슨 (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를 실행하여 모든 쓰기가 메모리에 도달했음을 보장해야 합니다. 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 API (drivers/nvdimm/pmem.c) */
static void pmem_submit_bio(struct bio *bio)
{
/* DAX 쓰기: memcpy 후 arch_wb_cache_pmem() 호출 */
__copy_from_iter(pmem_addr, &iter, len);
arch_wb_cache_pmem(pmem_addr, len); /* = CLWB 루프 + SFENCE */
nvdimm_flush(nd_region, bio);
}
/* 3) libpmem 사용 (userspace PMEM 라이브러리) */
/* pmem_persist(addr, len) → CLWB + SFENCE */
/* pmem_msync(addr, len) → msync() (ADR 보장이 없는 경우 fallback) */
/* pmem_is_pmem(addr, len) → /proc/iomem에서 PMEM 여부 확인 */
커널 DAX (Direct Access) 코드 경로
DAX는 파일시스템을 통해 PMEM에 직접(Page Cache 없이) 접근하는 메커니즘입니다. Page Cache를 우회하여 PMEM 주소에 직접 mmap/read/write를 수행합니다:
/* fs/dax.c — DAX 직접 접근 */
long dax_direct_access(struct dax_device *dax_dev, pgoff_t pgoff,
long nr_pages, enum dax_access_mode mode,
void **kaddr, pfn_t *pfn)
{
/* PMEM 물리 주소를 커널 가상 주소로 매핑 */
return dax_dev->ops->direct_access(dax_dev, pgoff, nr_pages,
mode, kaddr, pfn);
}
/* arch/x86/mm/pat/set_memory.c — DAX 영역은 Write-Back 캐시 가능 */
/* 단, 영속화를 위해 clwb + sfence가 쓰기 경로에 반드시 포함되어야 함 */
void dax_flush(struct dax_device *dax_dev, void *addr, size_t size)
{
if (unlikely(!dax_write_cache_enabled(dax_dev)))
arch_wb_cache_pmem(addr, size);
}
memcpy(pmem_addr, src, len)만 하면 데이터가 CPU 캐시에만 존재합니다. 전원 장애(ADR 환경) 시 더티 캐시 라인이 소실됩니다. 반드시 pmem_persist() 또는 arch_wb_cache_pmem() + nvdimm_flush()를 호출하세요.
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 */
| API | PAT 엔트리 | 용도 |
|---|---|---|
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() | WC | Write-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→디바이스 전송 전: 캐시를 플러시하여 메모리에 기록 */
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 도구를 사용하면 커널 수준에서 프로세스/코어별 캐시 미스를 실시간으로 분석할 수 있습니다:
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/
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
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) | 비고 |
|---|---|---|---|---|
| I | PrRd | E 또는 S | BusRd | 다른 캐시 hit → S, miss → E |
| I | PrWr | M | BusRdX | 배타적 소유권 획득 |
| S | PrRd | S | — | 로컬 히트, 버스 미사용 |
| S | PrWr | M | BusUpgr | invalidate만 전송 (데이터 불필요) |
| E | PrRd | E | — | 사일런트 히트 |
| E | PrWr | M | — | 사일런트 업그레이드 (핵심 최적화) |
| M | PrRd | M | — | 로컬 히트 |
| M | PrWr | M | — | 이미 배타적+dirty |
| M | BusRd | S | Flush | dirty 데이터 공급 + 메모리 갱신 |
| M | BusRdX | I | Flush | 소유권 이전 |
| E | BusRd | S | — | 공유 전환 (데이터 공급 가능) |
| S | BusRdX | I | — | 무효화 |
MOESI 확장: Owned 상태
AMD 프로세서에서 사용하는 MOESI는 Owned(O) 상태를 추가합니다. Modified 라인을 공유할 때 메모리에 write-back하지 않고 O 상태로 전환하여 dirty 데이터의 캐시 간 직접 전달을 가능하게 합니다.
| 상태 | 유효 | 배타적 | 더티 | 소유자 | 의미 |
|---|---|---|---|---|---|
| M | O | O | O | O | 유일한 사본, 메모리보다 새 값 |
| O | O | X | O | O | dirty 사본의 소유자, 다른 캐시에 S 사본 존재 |
| E | O | O | X | O | 유일한 사본, 메모리와 동일 |
| S | O | X | X | X | 공유 사본, 메모리와 동일 |
| I | X | — | — | — | 무효 |
/* 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] 비트로 확인 */
캐시 라인 내부 구조
캐시 라인은 단순한 64바이트 데이터 블록이 아니라, 태그(tag), 상태 비트, 데이터로 구성된 복합 구조입니다. CPU가 메모리 주소를 캐시에서 찾을 때 주소를 세 필드로 분해합니다.
/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 고급 이벤트
# 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의 기본 개념을 다루었으므로, 여기서는 실전 운영 시나리오와 커널 내부 구현을 심층적으로 살펴봅니다.
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):
* Bits [31:0] — RMID (Resource Monitoring ID)
* Bits [63:32] — CLOSID (Class of Service ID)
*
* 컨텍스트 스위치마다 이 MSR을 업데이트하여
* 태스크별 LLC 파티션과 모니터링 그룹을 전환합니다. */
intel-cmt-cat 사용자 공간(User Space) 도구나 rdt-config로 자동화할 수 있으며, 커널 6.5+에서는 resctrl의 cgroup 통합이 개선되었습니다.
ARM64 캐시 유지보수 명령어
ARM64는 x86의 완전한 하드웨어 코히런시와 달리, 소프트웨어 관리 캐시 유지보수가 필요한 시나리오가 있습니다. 특히 DMA, 자기 수정 코드(self-modifying code), 캐시 속성 변경 시 명시적 캐시 명령어를 사용해야 합니다.
커널 캐시 플러시 코드
/* 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. */
DSB ISH는 같은 클러스터 내 코어만 동기화하고, DSB OSH는 GPU/DMA 엔진까지 포함합니다. DMA 동기화에는 반드시 DSB SY 또는 DSB OSH를 사용해야 합니다.
캐시 컬러링과 페이지 할당
캐시 컬러링(page coloring)은 물리 페이지를 캐시 셋 매핑에 따라 분류하여, 서로 다른 가상 주소가 같은 캐시 셋을 과도하게 경쟁하는 것을 방지하는 기법입니다.
리눅스에서의 캐시 컬러링
메인라인 리눅스 커널은 명시적 캐시 컬러링을 하지 않습니다. 과거 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
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)를 유지하세요. */
DMA와 캐시 코히런시
DMA(Direct Memory Access) 엔진은 CPU 캐시를 우회하여 메모리에 직접 접근합니다. 코히런트 DMA(x86, 일부 ARM)에서는 하드웨어가 자동 동기화하지만, 비코히런트 DMA(대부분의 임베디드 ARM)에서는 소프트웨어가 명시적으로 캐시를 관리해야 합니다.
코히런트 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_cpu | sync_for_device |
|---|---|---|---|
DMA_TO_DEVICE | clean (dirty→메모리) | — | clean |
DMA_FROM_DEVICE | invalidate | invalidate | — |
DMA_BIDIRECTIONAL | clean+invalidate | invalidate | clean |
dma_sync_* 함수가 NOP으로 컴파일됩니다. 반면 대부분의 ARM SoC에서는 실제 캐시 유지보수 명령어가 실행되므로 성능에 영향을 줍니다.
캐시 앨리어싱 버그 사례
캐시 앨리어싱(aliasing)은 서로 다른 가상 주소가 동일한 물리 주소를 참조하면서 다른 캐시 셋에 매핑되어 같은 데이터의 서로 다른 캐시 사본이 존재하게 되는 문제입니다. 이는 VIPT(Virtually Indexed, Physically Tagged) 캐시에서 캐시 크기가 페이지 크기 × associativity를 초과할 때 발생합니다.
VIPT 앨리어싱 조건
| 캐시 구성 | Index+Offset 비트 | 페이지 크기 | 앨리어싱 | 이유 |
|---|---|---|---|---|
| 32KB 4-way | 6+6 = 12 | 4KB (12비트) | 없음 | 인덱스가 페이지 오프셋 내 |
| 32KB 8-way | 6+6 = 12 | 4KB (12비트) | 없음 | 인덱스가 페이지 오프셋 내 |
| 64KB 4-way | 8+6 = 14 | 4KB (12비트) | 있음! | 비트 [13:12]가 VA 의존 |
| 64KB 4-way | 8+6 = 14 | 16KB (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 수행. */
flush_dcache_page() 호출 여부를 점검하세요.
캐시 운영 진단 플레이북
캐시 관련 성능 문제를 체계적으로 진단하기 위한 단계별 가이드입니다. 증상에 따라 적절한 도구와 해결 방법을 선택합니다.
캐시 미스 종합 진단
#!/bin/bash
# cache_diagnosis.sh — 캐시 성능 종합 진단 스크립트
TARGET=$1
if [ -z "$TARGET" ]; then
echo "Usage: $0 "
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() 시스콜
* — 파일의 페이지 캐시 상태를 효율적으로 조회 */
#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("Cache hits: %llu\n", cs.nr_cache);
printf("Cache misses: %llu\n", cs.nr_dirty);
printf("Pages evicted:%llu\n", cs.nr_evicted);
printf("Recently evicted: %llu\n", cs.nr_recently_evicted);
/* mincore()보다 효율적:
* - 단일 시스콜로 전체 파일 통계 획득
* - hit/miss/eviction 정보 제공
* - 데이터베이스 버퍼 풀 관리에 유용
* - io_uring에서도 사용 가능 (IORING_OP_CACHESTAT)
*/
참고자료
공식 규격 및 표준
- Intel® 64 and IA-32 Architectures Software Developer's Manual — 캐시 계층, MESI 프로토콜, Intel RDT(Resource Director Technology)를 포함하는 공식 매뉴얼입니다
- Intel Resource Director Technology (RDT) — CAT(Cache Allocation Technology), MBA 등 캐시 파티셔닝 기술의 공식 문서입니다
- ARM Cortex-A Series Programmer's Guide — ARM 캐시 아키텍처, VIPT/PIPT, 캐시 유지보수 명령을 다룹니다
커널 문서
- Resource Control (resctrl) — Kernel Documentation — Intel RDT/AMD QoS 기반 캐시 및 메모리 대역폭 제어를 설명합니다
- CPU Topology — sysfs를 통한 캐시 공유 토폴로지 조회 방법입니다
- Cache and TLB Flushing Under Linux — 커널의 캐시/TLB 플러시 API를 설명합니다
- Performance Monitoring — Kernel Documentation — perf를 사용한 캐시 성능 측정 가이드입니다
LWN 기사
- What every programmer should know about memory (Part 1) — Ulrich Drepper의 메모리/캐시 심층 분석 시리즈입니다 (2007)
- What every programmer should know about memory (Part 3: CPU Caches) — 캐시 구조, associativity, 교체 정책을 상세히 설명합니다 (2007)
- Cache quality-of-service — Intel CAT/MBA를 활용한 캐시 QoS 기술을 다룹니다 (2016)
- perf c2c: Finding false sharing — perf c2c 도구를 사용한 false sharing 진단 방법을 설명합니다 (2018)
- Resizable arrays and false sharing — 커널 자료구조에서 false sharing 방지 패턴을 다룹니다 (2019)
커널 소스 코드
- arch/x86/kernel/cpu/cacheinfo.c — x86 캐시 정보 탐지 및 sysfs 내보내기 코드입니다
- arch/x86/kernel/cpu/resctrl/ — Intel RDT/AMD QoS resctrl 서브시스템 소스입니다
- drivers/base/cacheinfo.c — 아키텍처 공통 캐시 정보 프레임워크입니다
- tools/perf/builtin-c2c.c — perf c2c(cache-to-cache) false sharing 분석 도구 소스입니다
컨퍼런스 발표 및 기술 자료
- Brendan Gregg — perf Examples — perf를 사용한 캐시 미스 분석 예제를 포함하는 종합 가이드입니다
- What Every Programmer Should Know About Memory (PDF) — Ulrich Drepper의 캐시/메모리 분석 논문 전문입니다
관련 문서
CPU 캐시와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.