한 번에 끝내는 TLB(Translation Lookaside Buffer) — ‘주소 변환 캐시’ 이야기
들어가며 — 왜 TLB가 등장했을까?
가상 메모리를 구현할 때, CPU는 메모리 접근 전에 항상 “가상 주소 → 물리 주소” 변환을 해야 합니다. 그런데 이 변환 정보를 담은 페이지 테이블(Page Table)은 물리 메모리 어딘가에 저장되어 있죠. 결국 매번 메모리 접근할 때마다 “메모리에 한 번 더 다녀와야” 하는 겁니다.
이건 마치 카페에서 음료 한 잔 시킬 때마다 매번 냉장고 위치를 묻고 찾아가는 것과 같습니다. 너무 느리겠죠. 그래서 CPU 안에 “가장 자주 쓰는 변환 기록”을 따로 저장해두는 작은 캐시를 만들었어요. 바로 그게 TLB(Translation Lookaside Buffer)입니다.
TLB는 뭐하는 녀석인가?
TLB는 CPU 안의 MMU(Memory Management Unit)에 포함된 주소 변환 캐시입니다. 이름이 길지만 단순히 “최근에 쓴 가상→물리 주소 변환을 저장하는 작은 하드웨어 캐시”예요.
프로그램이 메모리를 읽을 때 CPU는 이렇게 행동합니다:
- 가상 주소에서 VPN(Virtual Page Number)을 추출
- TLB에서 VPN을 찾아본다
- 있으면 TLB Hit! → 바로 물리 주소로 변환 (빠름)
- 없으면 TLB Miss... → 페이지 테이블을 메모리에서 찾아서 TLB에 새로 넣고 다시 시도
결국 TLB가 히트하면 변환 속도가 “거의 0초”에 가까워지고, 미스하면 “페이지 테이블을 읽느라 느려지는” 거죠.
한눈에 보는 동작 알고리즘
if (VPN in TLB)
if (권한 확인 통과)
물리주소 = (PFN << offset) | offset
else
Protection Fault 발생
else
페이지 테이블 접근 → PTE 읽기
TLB에 추가 후 명령어 재시도
핵심은, CPU가 자동으로 “TLB → 페이지 테이블 → 다시 TLB” 순으로 처리한다는 점입니다. 대부분의 프로그램은 동일한 주소 근처를 자주 접근하기 때문에, TLB는 공간 지역성(spatial locality)과 시간 지역성(temporal locality)을 이용해 높은 효율을 냅니다.
예시 — 배열 접근에서 TLB가 하는 일
예를 들어 길이 10짜리 배열 a[10]을 순서대로 접근한다고 해봅시다. 페이지 크기가 16바이트라면, 이 배열은 3개의 페이지(VPN=6,7,8)에 걸쳐 저장됩니다.
a[0]을 읽으면 TLB 미스가 발생하지만, 같은 페이지에 있는 a[1], a[2]는 전부 히트합니다. a[3]에서 다시 미스가 나지만 a[4]~a[6]은 또 히트, 이런 식으로 반복됩니다.
총 10번의 접근에서 TLB 히트율은 70% 정도로 계산됩니다. 프로그램이 처음 데이터를 읽는데도 말이죠! 이유는 간단합니다 — “공간 지역성 덕분”입니다. 데이터가 한 페이지에 몰려 있으면 캐시 효과가 나거든요.
TLB 미스는 누가 처리할까?
여기서 중요한 질문이 하나 생깁니다. “TLB에 없으면, 누가 새로 채워주지?”
- 하드웨어가 직접 처리하는 방식 (x86 등):
CPU가 직접 페이지 테이블을 ‘walk’ 하며 PTE를 찾고, TLB에 넣습니다. - 운영체제가 처리하는 방식 (MIPS, SPARC 등):
하드웨어가 TLB Miss 예외를 발생시키면 OS가 트랩 핸들러를 실행해 직접 TLB를 갱신합니다.
이 차이는 역사적으로 CISC(복잡한 명령어 집합) vs RISC(단순한 명령어 집합) 설계 철학의 차이에서 비롯되었어요. 요즘은 두 방식을 절충해 하드웨어/OS가 협력하는 형태로 동작합니다.
🔁 컨텍스트 스위칭과 TLB
한 가지 함정이 있습니다. TLB 안에는 이전 프로세스의 주소 변환이 남아있을 수 있다는 점이에요. 그래서 새로운 프로세스로 전환할 때(TLB가 여전히 P1의 매핑을 갖고 있을 때), CPU가 엉뚱한 주소를 읽을 수도 있습니다.
해결법은 두 가지:
- TLB Flush: 컨텍스트 스위치 시 TLB 전체를 초기화 (간단하지만 성능 손해)
- ASID(Address Space ID): 각 엔트리에 “이게 어떤 프로세스의 매핑인지” 표시 (성능 향상)
ASID는 TLB의 “학생증” 같은 겁니다. 각 프로세스가 고유한 번호를 갖고 있어서 여러 프로세스의 매핑을 동시에 캐시에 보관할 수 있죠.
실제 TLB의 구조 (MIPS 예시)
VPN | G | ASID | PFN | C | D | V
- VPN: 가상 페이지 번호
- PFN: 물리 프레임 번호
- ASID: 프로세스 식별자
- G(Global): 공유 페이지 표시
- D(Dirty): 쓰기된 페이지
- V(Valid): 유효한 매핑 여부
MIPS에서는 OS가 직접 TLB를 관리합니다. 예를 들어 TLBP(검색), TLBR(읽기), TLBWI(특정 엔트리 교체), TLBWR(랜덤 엔트리 교체) 같은 명령어를 사용하죠. 이런 명령어는 모두 특권 명령(privileged instruction)이라 사용자 프로그램은 건드릴 수 없습니다.
성능 이야기 — TLB 커버리지
프로그램이 한꺼번에 너무 많은 페이지를 접근하면, TLB가 그걸 다 담지 못합니다. 그럼 “TLB 커버리지를 초과”했다고 하며, 성능이 확 떨어집니다. 대형 데이터베이스나 그래픽 엔진 같은 프로그램이 대표적이에요.
그래서 현대 시스템은 “큰 페이지(large page)”나 “멀티 레벨 TLB”를 지원해서 커버리지를 넓히는 식으로 최적화합니다.
마무리 — 결국 TLB는 ‘속도의 마법사’
페이징만으로는 주소 변환이 너무 느려집니다. 하지만 TLB 덕분에 대부분의 주소 변환이 CPU 내부에서 즉시 해결돼요. 이 덕분에 우리는 “가상 메모리의 편리함”과 “물리 메모리의 속도”를 동시에 누릴 수 있게 된 겁니다.
기억하세요. TLB는 하드웨어 수준의 캐시이며, 성능의 핵심입니다. 페이지 테이블의 교과서적인 개념이 현실에서 빛나는 건, 바로 이 작은 버퍼 덕분이에요.