> [!abstract] Introduction > CPU가 메모리에 접근할 때마다 [페이지 테이블](Virtualization.md)을 뒤지면 어떻게 될까요? 한 번의 메모리 접근에 수 차례의 메모리 접근이 추가로 필요해지면서 성능이 급격히 떨어집니다. TLB*Translation Lookaside Buffer*는 이 문제를 해결하기 위해 등장한 하드웨어 캐시로, 최근 사용된 주소 변환 결과를 저장하여 페이지 테이블 워크 없이 즉시 물리 주소를 얻을 수 있게 합니다. 이 글에서는 TLB의 기본 구조와 동작 원리부터 다단계 TLB 계층, 관리 정책, 컨텍스트 스위치에서의 처리, 그리고 Huge Page와 KPTI까지 포괄적으로 다룹니다. ## 왜 TLB가 필요한가 [Virtualization](Virtualization.md) 글에서 살펴본 것처럼, 오늘날 모든 프로세스는 가상 주소*virtual address*를 사용하며, MMU*Memory Management Unit*가 페이지 테이블을 참조하여 가상 주소를 물리 주소로 변환한다. 문제는 이 페이지 테이블이 메인 메모리에 존재한다는 점이다. x86-64의 4단계 페이지 테이블(PML4 → PDP → PD → PT)[^pml]을 예로 들면, 하나의 가상 주소를 물리 주소로 변환하기 위해 메모리를 네 번 추가로 읽어야 한다. 5단계 페이징(`PML5`)을 사용하면 다섯 번이다. 메인 메모리 접근 한 번에 약 50~100ns가 소요되므로, 페이지 테이블 워크*page table walk*만으로도 200~500ns가 추가된다. 원래 하려던 데이터 접근까지 합치면 한 번의 메모리 참조에 총 다섯 번의 메모리 접근이 필요한 셈이다. $ T_{\text{effective}} = T_{\text{page table walk}} + T_{\text{memory access}} = 4 \times T_{\text{mem}} + T_{\text{mem}} = 5 \times T_{\text{mem}} $ 이 오버헤드를 해결하기 위해 참조 지역성*locality of reference*[^locality]이라는 프로그램의 본질적 특성을 활용한다. 프로그램은 최근에 접근한 주소 근처를 다시 접근하는 경향이 강하므로, 최근 변환 결과를 빠른 하드웨어 캐시에 저장해두면 대부분의 변환을 메모리 접근 없이 처리할 수 있다. 이 캐시가 바로 TLB다. ## TLB의 기본 구조 TLB는 MMU 내부에 위치한 소규모의 고속 캐시로, 가상 페이지 번호*Virtual Page Number, VPN*에서 물리 프레임 번호*Physical Frame Number, PFN*로의 변환 결과를 저장한다. 일반적인 CPU 데이터 캐시가 메모리 내용을 캐싱하는 것과 달리, TLB는 주소 변환 결과만을 전문적으로 캐싱한다. ### TLB 엔트리 하나의 TLB 엔트리는 페이지 테이블 엔트리*Page Table Entry, PTE*의 핵심 정보를 압축한 것으로, 다음과 같은 필드로 구성된다. | 필드 | 크기 (예시) | 설명 | |------|-----------|------| | VPN | 가변 | 가상 페이지 번호 — 검색 키 | | PFN | 가변 | 물리 프레임 번호 — 변환 결과 | | Valid | 1비트 | 이 엔트리가 유효한지 여부 | | Dirty | 1비트 | 해당 페이지에 쓰기가 발생했는지 여부 | | Reference | 1비트 | 최근에 접근되었는지 여부 (교체 알고리즘에 활용) | | Permission | 2~3비트 | 읽기/쓰기/실행 권한 | | ASID/PCID | 8~16비트 | 주소 공간 식별자 (선택적) | | Global | 1비트 | 모든 주소 공간에서 유효한 전역 매핑 여부 | `Global` 비트가 설정된 엔트리는 커널 매핑처럼 모든 프로세스가 공유하는 페이지에 사용되며, TLB 플러시 시에도 제거되지 않는다. ### 연관 매핑 방식 TLB는 검색 속도가 생명이므로, 대부분 완전 연관*fully associative* 또는 집합 연관*set-associative* 방식으로 설계된다. **완전 연관 TLB**에서는 VPN이 모든 엔트리와 동시에 비교된다. CAM*Content-Addressable Memory*[^cam] 회로를 사용하여 단일 사이클에 매칭을 수행하므로 검색 성능이 뛰어나지만, 엔트리 수가 늘어날수록 회로 면적과 전력 소비가 급격히 증가한다. 그래서 완전 연관 TLB는 보통 수십 개 이내의 소규모 구조에 사용된다. **집합 연관 TLB**는 VPN의 일부 비트로 집합*set*을 선택하고, 해당 집합 내의 여러 웨이*way*를 병렬 비교한다. 데이터 캐시의 집합 연관 구조와 동일한 원리다. 4-way나 8-way가 일반적이며, 수백 개의 엔트리를 수용하면서도 탐색 시간을 1~2사이클로 유지할 수 있다. ## TLB 동작 흐름 CPU가 가상 주소를 발행하면 MMU는 먼저 TLB를 조회한다. 이 과정은 크게 **TLB 히트**와 **TLB 미스** 두 경로로 나뉜다. ### TLB 히트 가상 주소에서 추출한 VPN이 TLB 엔트리의 VPN과 일치하고, 유효 비트가 설정되어 있으며, 권한 검사를 통과하면 TLB 히트다. TLB가 즉시 PFN을 반환하고, 이 PFN에 페이지 오프셋을 결합하여 물리 주소가 완성된다. 추가 메모리 접근 없이 1~2사이클 만에 주소 변환이 완료되므로, 마치 페이지 테이블이 존재하지 않는 것처럼 빠르다. ### TLB 미스 VPN이 TLB에 없으면 TLB 미스가 발생한다. 이때 **페이지 테이블 워크**가 시작된다. 하드웨어 또는 소프트웨어가 메인 메모리의 페이지 테이블을 순차적으로 탐색하여 최종 PTE를 찾고, 그 결과를 TLB에 채워 넣는다. 이후 같은 VPN에 대한 접근은 TLB 히트로 처리된다. 페이지 테이블 워크의 구현 방식에 따라 두 가지 모델이 존재한다. **하드웨어 관리 TLB**는 x86, ARM 등 대부분의 현대 아키텍처가 채택한 방식이다. MMU 내부의 전용 하드웨어(Page Table Walker)가 자동으로 페이지 테이블을 탐색하여 TLB를 채운다. 운영체제 개입 없이 하드웨어가 처리하므로 미스 페널티가 상대적으로 낮다. **소프트웨어 관리 TLB**는 MIPS, SPARC 등 일부 RISC 아키텍처에서 사용한다. TLB 미스가 발생하면 예외*exception*가 트리거되고, 운영체제의 TLB 미스 핸들러가 페이지 테이블을 탐색하여 `TLBWR`(TLB Write Random) 같은 명령어로 직접 엔트리를 삽입한다. 하드웨어가 단순해지는 대신, 미스 처리 지연이 길고 핸들러 코드의 품질이 성능에 직접적인 영향을 미친다. ![[tlbLookupFlow.svg]] ### TLB 미스 vs. Page Fault TLB 미스와 Page Fault[^page-fault]는 혼동하기 쉽지만 본질적으로 다른 이벤트다. TLB 미스는 변환 결과가 TLB 캐시에 없을 뿐이지, 페이지 자체는 물리 메모리에 있을 수 있다. 페이지 테이블 워크를 통해 해결되며 수백 나노초 수준의 지연이 발생한다. 반면 Page Fault는 페이지 테이블 워크 결과 해당 페이지가 물리 메모리에 존재하지 않는 경우(PTE의 Present 비트가 0)에 발생하며, 디스크에서 페이지를 로드해야 하므로 수 밀리초에서 수십 밀리초의 지연이 동반된다. | 구분 | TLB 미스 | Page Fault | |------|---------|------------| | 원인 | 변환 결과가 TLB에 없음 | 페이지가 물리 메모리에 없음 | | 처리 | 페이지 테이블 워크 (하드웨어/소프트웨어) | OS의 페이지 폴트 핸들러 | | 지연 | 약 10~200ns | 약 1~10ms (디스크 I/O 포함) | | 빈도 | 상대적으로 흔함 | 상대적으로 드묾 | ## 유효 접근 시간 TLB의 성능은 **히트율***hit ratio*로 결정된다. 히트율 $h$일 때 유효 메모리 접근 시간*Effective Access Time, EAT*은 다음과 같다. $ \text{EAT} = h \times (T_{\text{TLB}} + T_{\text{mem}}) + (1 - h) \times (T_{\text{TLB}} + T_{\text{walk}} + T_{\text{mem}}) $ 여기서 $T_{\text{TLB}}$는 TLB 조회 시간(약 1ns 미만), $T_{\text{mem}}$은 메모리 접근 시간(약 50~100ns), $T_{\text{walk}}$는 페이지 테이블 워크 시간이다. 4단계 페이징에서 $T_{\text{walk}} = 4 \times T_{\text{mem}}$이므로, $T_{\text{mem}} = 100\text{ns}$, $h = 99\%$라 가정하면: $ \text{EAT} = 0.99 \times (1 + 100) + 0.01 \times (1 + 400 + 100) = 99.99 + 5.01 = 105\text{ns} $ TLB 없이 매번 페이지 테이블을 워크하면 $500\text{ns}$가 필요하므로, 99%의 히트율만으로도 약 4.7배의 성능 향상을 얻는다. 실제 워크로드에서 TLB 히트율은 일반적으로 99% 이상에 달한다. ## 다단계 TLB 계층 현대 프로세서는 단일 TLB가 아닌 **다단계 TLB 계층**을 사용한다. 데이터 캐시가 L1, L2, L3로 나뉘듯, TLB도 계층화되어 있다. ### L1 TLB: 명령어 TLB와 데이터 TLB L1 TLB는 CPU 파이프라인에 가장 가까이 위치하여 1~2사이클 이내에 응답한다. 명령어 페치와 데이터 접근이 동시에 일어날 수 있으므로, L1 TLB는 **명령어 TLB**(iTLB)와 **데이터 TLB**(dTLB)로 분리된다. 분리 설계는 두 경로가 서로 간섭 없이 병렬로 주소 변환을 수행할 수 있게 하며, 파이프라인 정체를 방지한다. ### L2 TLB: 통합 TLB L1 TLB에서 미스가 발생하면 L2 TLB가 조회된다. L2 TLB는 명령어와 데이터 변환을 통합하여 관리하며, L1보다 훨씬 많은 엔트리를 보유한다. 접근 지연은 약 4~8사이클이지만, L2 TLB에서 히트하면 페이지 테이블 워크를 회피할 수 있다. ![[tlbHierarchy.svg]] ### 실제 프로세서 TLB 사양 비교 | 프로세서 | L1 iTLB | L1 dTLB | L2 TLB | |---------|---------|---------|--------| | Intel Alder Lake (P-core) | 256 엔트리 (8-way) | 96 엔트리 (fully) | 2,048 엔트리 (16-way) | | AMD Zen 4 | 64 엔트리 (fully) | 72 엔트리 (fully) | 3,072 엔트리 (12-way) | | ARM Cortex-A710 | 48 엔트리 (fully) | 48 엔트리 (fully) | 1,024 엔트리 (4-way) | | Apple M2 (Avalanche) | 256 엔트리 | 160 엔트리 | 4,096 엔트리 | 엔트리 수가 수십~수천 개에 불과한 것은 TLB가 전용 CAM/SRAM 회로로 구현되어 면적과 전력 비용이 높기 때문이다. 그럼에도 높은 히트율을 유지할 수 있는 것은 참조 지역성 덕분이다. ## TLB 관리 정책 ### 교체 정책 TLB가 가득 찬 상태에서 새 엔트리를 삽입해야 하면, 기존 엔트리 하나를 교체해야 한다. 일반적인 교체 정책은 다음과 같다. **LRU**(Least Recently Used)는 가장 오래 사용되지 않은 엔트리를 교체한다. 참조 지역성을 잘 반영하지만, 완전 연관 TLB에서 정확한 LRU를 구현하려면 엔트리 간의 접근 순서를 추적하는 하드웨어가 필요하다. 실제로는 의사*pseudo*-LRU를 사용하는 경우가 많다. **라운드 로빈**(Round Robin)은 순환 포인터가 가리키는 위치의 엔트리를 교체한다. 구현이 단순하고 하드웨어 비용이 낮아 일부 임베디드 프로세서에서 사용된다. **랜덤**(Random)은 무작위로 엔트리를 선택하여 교체한다. 병리적*pathological* 접근 패턴에 강하고 구현이 단순하다. ARM TLB에서 소프트웨어 관리 시 `TLBWR` 명령어가 이 방식을 사용한다. ### TLB 플러시 TLB 엔트리는 페이지 테이블의 캐시이므로, 페이지 테이블이 변경되면 TLB의 해당 엔트리도 무효화해야 한다. 이를 **TLB 플러시**(또는 TLB 무효화*invalidation*)라 한다. 플러시가 필요한 대표적인 상황은 다음과 같다. **프로세스 전환**: 새 프로세스의 페이지 테이블로 교체할 때, 이전 프로세스의 TLB 엔트리가 잘못된 물리 주소를 가리킬 수 있으므로 플러시한다. [[Context Switch|컨텍스트 스위치]]에서 `CR3` 레지스터를 변경하면 x86에서는 기본적으로 전체 TLB가 플러시된다. **페이지 테이블 수정**: `mprotect()`로 권한을 변경하거나, `munmap()`으로 매핑을 해제하거나, 페이지 스왑 아웃 시 해당 PTE에 대응하는 TLB 엔트리를 개별적으로 무효화한다. x86에서는 `INVLPG` 명령어가 단일 가상 주소의 TLB 엔트리를 무효화한다. **전체 플러시**: 대규모 매핑 변경(예: `exec()` 호출로 새 프로그램 로드) 시에는 개별 무효화보다 전체 플러시가 효율적이다. ### TLB Shootdown 멀티프로세서 시스템에서 하나의 코어가 페이지 테이블을 수정하면, 다른 코어들의 TLB에도 해당 변환이 캐싱되어 있을 수 있다. 이 오래된*stale* 엔트리를 동기적으로 제거하는 과정을 **TLB shootdown**이라 한다. 리눅스 커널에서의 일반적인 TLB shootdown 절차는 다음과 같다. 1. 소스 코어가 페이지 테이블을 수정한다. 2. 소스 코어가 해당 프로세스가 실행 중인 모든 원격 코어에 IPI*Inter-Processor Interrupt*[^ipi]를 전송한다. 3. IPI를 수신한 각 코어는 인터럽트 핸들러에서 지정된 주소 범위의 TLB 엔트리를 무효화한다. 4. 소스 코어는 모든 원격 코어의 무효화 완료를 확인한 후 진행한다. TLB shootdown은 IPI 전송과 동기화 대기로 인해 수 마이크로초의 지연을 유발한다. 코어 수가 늘어날수록 비용이 증가하므로, 대규모 서버에서는 성능 병목이 될 수 있다. 최신 프로세서들은 이 문제를 하드웨어로 해결하려는 시도를 하고 있는데, AMD의 `INVLPGB`*Invalidate Page Global Broadcast* 명령어와 Intel의 RAR*Remote Action Request*가 대표적이다. 이들은 IPI 없이 하드웨어가 직접 원격 TLB를 무효화하여 shootdown 오버헤드를 크게 줄인다. ## 컨텍스트 스위치와 TLB [[Context Switch|컨텍스트 스위치]]에서 프로세스 간 전환이 발생하면, 이전 프로세스의 TLB 엔트리가 새 프로세스에게 잘못된 매핑을 제공할 위험이 있다. 이 문제를 해결하는 방식은 아키텍처에 따라 크게 다르다. ![[tlbContextSwitch.svg]] ### 무조건 플러시 (기본 동작) 가장 단순한 방법은 전환 시 TLB를 전부 비우는 것이다. x86에서 `PCID`를 사용하지 않으면 `CR3`에 새 페이지 테이블 주소를 쓰는 순간 모든 non-global TLB 엔트리가 자동으로 플러시된다. 안전하지만, 전환 직후 새 프로세스의 모든 메모리 접근에서 TLB 미스가 발생하여 성능이 크게 저하된다. 이것이 [[Context Switch|컨텍스트 스위치]]의 간접 비용 중 가장 큰 부분이다. ### PCID (x86) `PCID`*Process Context Identifier*는 x86에서 TLB 엔트리에 12비트 식별자를 태깅하여, `CR3` 교체 시 전체 플러시를 회피하는 기법이다. `CR3`의 하위 12비트에 PCID 값을 포함시키고, bit 63을 1로 설정하면 **no-flush 로드**가 수행된다. 하드웨어는 TLB 조회 시 현재 PCID와 엔트리의 PCID를 자동으로 비교하여, 다른 프로세스의 엔트리가 히트되지 않도록 보장한다. ![[tlbCr3Layout.svg]] 12비트이므로 최대 4,096개의 PCID를 동시에 사용할 수 있다. 리눅스 커널은 per-CPU로 PCID를 관리하며, 코어당 최근 실행된 프로세스들에 PCID를 할당한다. PCID가 부족하면 가장 오래된 PCID를 재활용하면서 해당 PCID의 TLB 엔트리를 플러시한다. `INVPCID` 명령어를 사용하면 특정 PCID의 엔트리만 선택적으로 무효화할 수 있다. PCID는 스위치 집약적 워크로드에서 TLB 미스 페널티를 **10~40%** 줄이는 효과가 있다. ### ASID (ARM) `ASID`*Address Space Identifier*는 ARM 아키텍처에서 TLB 엔트리에 주소 공간 식별자를 태깅하는 기법으로, x86의 PCID와 동일한 목적을 가진다. ASID는 `TTBR0_EL1`[^ttbr]의 상위 비트에 인코딩되며, 크기는 구현에 따라 8비트(최대 256) 또는 16비트(최대 65,536)다. ARM의 설계는 x86보다 구조적으로 유리하다. 사용자 공간 페이지 테이블(`TTBR0_EL1`)과 커널 페이지 테이블(`TTBR1_EL1`)이 처음부터 분리되어 있으므로, 프로세스 전환 시 `TTBR0`만 변경하면 된다. 커널 매핑은 `TTBR1`에 고정되어 있어 전혀 영향을 받지 않는다. 16비트 ASID(ARMv8.2+)는 최대 65,536개의 주소 공간을 동시에 태깅할 수 있어 롤오버가 거의 발생하지 않는다. ASID가 고갈되면 커널은 **세대 롤오버***generation rollover*를 수행한다. 세대 카운터를 증가시키고, 전체 TLB를 플러시한 뒤 ASID를 재할당한다. 16비트 ASID에서는 이 이벤트가 매우 드물다. | 항목 | PCID (x86) | ASID (ARM) | |------|-----------|-----------| | 크기 | 12비트 (4,096) | 8비트 (256) 또는 16비트 (65,536) | | 위치 | `CR3` 하위 12비트 | `TTBR0_EL1` 상위 비트 | | 페이지 테이블 레지스터 | `CR3` 하나 (전체 주소 공간) | `TTBR0` (사용자) + `TTBR1` (커널) 분리 | | 커널 매핑 전환 | 필요 (`CR3`가 전체를 커버) | 불필요 (`TTBR1` 고정) | | 선택적 무효화 | `INVPCID` | `TLBI ASIDE1IS`, `TLBI VAE1IS` | ### Lazy TLB 커널 스레드는 고유한 사용자 공간 주소 공간(`mm_struct`)을 갖지 않는다. [[Context Switch|컨텍스트 스위치]]에서 살펴본 것처럼, 커널 스레드로 전환할 때 리눅스는 이전 태스크의 `active_mm`을 빌려 쓰는 **Lazy TLB** 최적화를 적용한다. 커널 스레드는 커널 주소 공간만 사용하므로 사용자 공간의 TLB 엔트리에 접근할 일이 없고, 따라서 `CR3`/`TTBR0` 변경 없이 기존 TLB 엔트리를 그대로 유지할 수 있다. 이후 사용자 프로세스로 복귀할 때 비로소 주소 공간 전환이 수행된다. ## Huge Page와 TLB 효율 표준 4KB 페이지를 사용할 때, 하나의 TLB 엔트리는 4KB의 가상 주소 공간만 커버한다. 2,048개의 TLB 엔트리를 가진 L2 TLB가 커버하는 총 주소 공간은 겨우 8MB에 불과하다. 대규모 메모리를 사용하는 애플리케이션(데이터베이스, 과학 계산, 가상 머신 등)에서는 이 커버리지가 부족하여 TLB 미스율이 급증한다. **Huge Page**[^huge-page]는 페이지 크기를 키워서 TLB 커버리지를 극적으로 늘린다. | 페이지 크기 | 2,048 엔트리 TLB 커버리지 | 대비 | |-----------|------------------------|------| | 4KB | 8MB | 1× | | 2MB | 4GB | 512× | | 1GB | 2TB | 262,144× | x86-64에서 2MB Huge Page는 PD*Page Directory* 단계에서 변환이 종료되므로 페이지 테이블 워크가 3단계로 줄어들고, 1GB Huge Page는 PDP*Page Directory Pointer* 단계에서 종료되어 2단계만 필요하다. 페이지 테이블 워크 단계 감소와 TLB 커버리지 증가가 결합되어 큰 성능 향상을 기대할 수 있다. 리눅스에서는 `hugetlbfs`를 통해 명시적으로 Huge Page를 사용하거나, **THP**(Transparent Huge Pages)를 활성화하여 커널이 자동으로 연속된 4KB 페이지를 2MB Huge Page로 승격시킬 수 있다. ## KPTI와 TLB `KPTI`*Kernel Page Table Isolation*[^kpti]는 2018년 발견된 Meltdown[^meltdown] 취약점을 완화하기 위해 도입된 기법으로, TLB 관리에 직접적인 영향을 미친다. Meltdown 이전에는 프로세스의 페이지 테이블 하나에 사용자 공간과 커널 공간 매핑이 모두 포함되어 있었다. 커널 매핑은 권한 비트로 보호되어 사용자 모드에서 접근할 수 없었지만, Meltdown 공격은 CPU의 투기적 실행*speculative execution*을 악용하여 이 보호를 우회했다. KPTI는 프로세스마다 **두 세트의 페이지 테이블**을 유지한다. 사용자 모드에서는 커널 매핑이 최소한으로만 포함된 페이지 테이블을, 커널 모드에서는 전체 매핑이 포함된 페이지 테이블을 사용한다. 시스템 콜마다 `CR3`를 두 번 전환해야 하므로(사용자 → 커널, 커널 → 사용자), TLB에 대한 영향이 크다. PCID가 이 비용을 상당 부분 상쇄한다. 리눅스는 사용자 페이지 테이블과 커널 페이지 테이블에 서로 다른 PCID를 할당하여, `CR3` 전환 시 no-flush 로드를 수행한다. 그럼에도 syscall 집약적 워크로드에서 **5~30%**의 추가 오버헤드가 관측된다. ARM(AArch64)은 `TTBR0`/`TTBR1` 분리 설계 덕분에 대부분의 코어에서 KPTI가 불필요하다. 커널 매핑은 `TTBR1`에만 존재하고 `EL0`(사용자 모드)에서 접근할 수 없도록 하드웨어 수준에서 격리되어 있기 때문이다. Meltdown에 영향을 받은 ARM 코어는 Cortex-A75 정도에 한정된다. ## TLB와 성능 ### TLB 커버리지 계산 TLB의 실질적 효용을 판단하는 핵심 지표는 **TLB 커버리지**, 즉 TLB가 동시에 매핑할 수 있는 총 가상 주소 공간의 크기다. $ \text{TLB Coverage} = \text{TLB Entries} \times \text{Page Size} $ 프로세스의 워킹 셋*working set*이 TLB 커버리지 안에 들어오면 거의 모든 접근이 TLB 히트가 된다. 워킹 셋이 커버리지를 초과하면 TLB 미스가 급증하는데, 이를 **TLB 스래싱***thrashing*이라 한다. ### TLB 미스 페널티의 실질적 영향 TLB 미스 하나의 비용은 아키텍처와 메모리 계층에 따라 다르지만, 대략적인 수치는 다음과 같다. | 시나리오 | 예상 지연 | |---------|----------| | TLB 히트 | 약 0.5~1ns (1~2사이클) | | L2 TLB 히트 | 약 2~4ns (4~8사이클) | | TLB 미스 + 페이지 워크 (캐시 히트) | 약 10~30ns | | TLB 미스 + 페이지 워크 (캐시 미스) | 약 50~200ns | | TLB 미스 + Page Fault | 약 1~10ms | 데이터베이스나 가상 머신처럼 대규모 메모리를 랜덤 접근하는 워크로드에서는 TLB 미스가 주요 성능 병목이 될 수 있다. 이런 경우 Huge Page 적용, 워킹 셋 최적화, 데이터 구조의 공간 지역성 개선 등이 효과적인 대응 방법이다. ### 성능 최적화 기법 요약 | 기법 | 원리 | 효과 | |------|------|------| | PCID / ASID | TLB 엔트리에 프로세스 태깅 | 컨텍스트 스위치 시 TLB 플러시 회피 | | Huge Page | 페이지 크기 확대 | TLB 커버리지 512~262,144배 증가 | | CPU 어피니티 | 프로세스를 특정 코어에 고정 | 웜 TLB 유지 | | Superpages | OS가 연속 페이지를 자동 합병 | 투명하게 TLB 효율 향상 (THP) | | 하드웨어 원격 무효화 | `INVLPGB`, RAR | TLB shootdown에서 IPI 제거 | --- [^pml]: `PML4`(Page Map Level 4)와 `PML5`(Page Map Level 5)는 x86-64의 다단계 페이지 테이블 계층에서 최상위 테이블이다. `PML4`는 4단계 변환으로 48비트 가상 주소(256TB)를, `PML5`는 5단계 변환으로 57비트 가상 주소(128PB)를 지원한다. — Intel SDM Volume 3, Chapter 4 "Paging". [^locality]: 참조 지역성*locality of reference*은 프로그램이 최근에 접근한 메모리 주소(시간 지역성*temporal locality*) 또는 그 근처의 주소(공간 지역성*spatial locality*)를 가까운 미래에 다시 접근할 가능성이 높다는 관찰이다. 캐시, TLB, 프리페칭 등 현대 컴퓨터 아키텍처의 대부분의 최적화가 이 특성에 기반한다. — Hennessy and Patterson, *Computer Architecture: A Quantitative Approach* 6th ed., Chapter 2. [^cam]: CAM(Content-Addressable Memory)은 저장된 모든 항목을 입력 검색어와 동시에 비교하여 단일 사이클에 매칭 결과를 반환하는 특수 메모리다. 일반 RAM이 주소로 데이터를 찾는 반면, CAM은 데이터로 주소를 찾는다. TLB, 네트워크 라우터의 룩업 테이블 등에 사용된다. — Pagiamtzis and Sheikholeslami, "Content-addressable memory (CAM) circuits and architectures: A tutorial and survey," *IEEE Journal of Solid-State Circuits*, vol. 41, no. 3, 2006. [^page-fault]: Page Fault는 CPU가 접근하려는 가상 주소에 대응하는 물리 페이지가 현재 메인 메모리에 존재하지 않을 때 발생하는 예외다. 운영체제가 디스크(스왑 영역 또는 파일 시스템)에서 해당 페이지를 물리 메모리로 가져온 뒤 실행을 재개한다. — Silberschatz et al., *Operating System Concepts* 10th ed., §10.2. [^ipi]: IPI(Inter-Processor Interrupt)는 멀티프로세서 시스템에서 하나의 코어가 다른 코어에 직접 전달하는 인터럽트다. 리눅스에서는 TLB shootdown, 스케줄러 리밸런싱, `smp_call_function()` 등에 사용된다. x86에서는 LAPIC(Local Advanced Programmable Interrupt Controller)를 통해 전송된다. — Bovet and Cesati, *Understanding the Linux Kernel* 3rd ed., Chapter 4. [^ttbr]: `TTBR`(Translation Table Base Register)는 ARM의 페이지 테이블 베이스 레지스터다. `TTBR0_EL1`은 하위 가상 주소 범위(사용자 공간)의 페이지 테이블을, `TTBR1_EL1`은 상위 가상 주소 범위(커널 공간)의 페이지 테이블을 가리킨다. 이 분리 설계가 ARM에서 커널 매핑 전환 없이 프로세스 전환을 가능하게 한다. — ARM Architecture Reference Manual for A-profile, Chapter D8. [^huge-page]: Huge Page(대형 페이지)는 표준 4KB보다 큰 페이지 크기를 사용하여 TLB 효율을 높이는 기법이다. x86-64에서는 2MB(PDE 직접 매핑)와 1GB(PDPE 직접 매핑)를 지원한다. 리눅스에서는 `hugetlbfs`(명시적)와 THP(Transparent Huge Pages, 투명)로 사용할 수 있다. — Linux kernel documentation, `Documentation/admin-guide/mm/hugetlbpage.rst`. [^kpti]: KPTI(Kernel Page Table Isolation)는 사용자 모드에서 커널 페이지 테이블에 접근할 수 없도록 사용자/커널 페이지 테이블을 분리하는 기법이다. Meltdown 취약점(CVE-2017-5754) 완화를 위해 리눅스 4.15에서 도입되었다. — Linux kernel documentation, `Documentation/admin-guide/hw-vuln/meltdown.rst`. [^meltdown]: Meltdown(CVE-2017-5754)은 Intel CPU의 투기적 실행*speculative execution*을 악용하여, 사용자 모드에서 커널 메모리를 읽을 수 있는 하드웨어 취약점이다. 투기적으로 실행된 명령어가 권한 검사 전에 데이터를 캐시에 로드하는 점을 이용하여, 사이드 채널 공격으로 커널 데이터를 추출한다. — Lipp, M. et al. (2018) "Meltdown: Reading Kernel Memory from User Space," *27th USENIX Security Symposium*. ## 출처 - Hennessy, J. L. and Patterson, D. A. (2019) *Computer Architecture: A Quantitative Approach*. 6th ed. Cambridge, MA: Morgan Kaufmann. - Patterson, D. A. and Hennessy, J. L. (2021) *Computer Organization and Design MIPS Edition: The Hardware/Software Interface*. 6th ed. Cambridge, MA: Morgan Kaufmann. - Silberschatz, A., Galvin, P. B., and Gagne, G. (2018) *Operating System Concepts*. 10th ed. Hoboken, NJ: Wiley. - Arpaci-Dusseau, R. H. and Arpaci-Dusseau, A. C. (2023) *Operating Systems: Three Easy Pieces*. 1.10 ed. Arpaci-Dusseau Books. - Bovet, D. P. and Cesati, M. (2008) *Understanding the Linux Kernel*. 3rd ed. Sebastopol, CA: O'Reilly. - Love, R. (2010) *Linux Kernel Development*. 3rd ed. Upper Saddle River, NJ: Addison-Wesley. - Intel Corporation. *Intel® 64 and IA-32 Architectures Software Developer's Manual*. Volume 3, Chapter 4 "Paging". - AMD. *AMD64 Architecture Programmer's Manual — System Programming*. Volume 2. - ARM. *ARM Architecture Reference Manual for A-profile*. Chapter D8 "The AArch64 Virtual Memory System Architecture". - Lipp, M. et al. (2018) "Meltdown: Reading Kernel Memory from User Space," *27th USENIX Security Symposium*. - Linux kernel source v6.19 — `arch/x86/mm/tlb.c`, `arch/arm64/mm/context.c`. - Linux kernel documentation — `Documentation/admin-guide/mm/hugetlbpage.rst`, `Documentation/admin-guide/hw-vuln/meltdown.rst`.