한양과학기술고등학교

에듀테크 프로젝트 대시보드

TLB (Translation Lookaside Buffer)

TLB — 주소 변환을 위한 캐시

가상 메모리 & 페이징에서 CPU가 쓰는 모든 주소가 페이지 테이블을 거쳐 물리 주소로 바뀐다는 걸 배웠다. 문제는 그 페이지 테이블도 결국 메모리(RAM) 안에 있다는 것 — 그래서 이번엔 그 변환 자체를 캐시 메모리 매핑에서 배운 방식으로 캐싱하는 TLB(Translation Lookaside Buffer)를 다룬다.

1. 문제 상황 복습 — 주소 변환도 메모리 접근이다

프로그램이 읽으려는 데이터 하나에 접근하려면, 실제로는 메모리에 두 번 접근해야 한다.

① 페이지 테이블 조회가상주소 → 물리주소 매핑을 찾음
메모리 접근 1회
물리주소 확정PFN + offset 조합
② 실제 데이터 읽기원하던 값을 진짜로 읽음
메모리 접근 1회 더
결론: 아무 최적화가 없으면 메모리 접근 1번이 사실은 2번(페이지 테이블 + 데이터)이 된다. 페이지 테이블이 다단계면 더 늘어난다. 페이징의 원리 자체는 가상 메모리 & 페이징에서 이미 다뤘으니 여기서는 반복하지 않는다.

2. 해결책: TLB — "주소 변환 결과"를 캐싱한다

캐시 메모리 매핑에서 배운 캐시는 "자주 쓰는 데이터를 CPU 가까이 작은 초고속 메모리에 복사해 둔다"는 아이디어였다. TLB는 정확히 같은 아이디어를 데이터가 아니라 주소 변환 결과(가상 페이지 번호 VPN → 물리 프레임 번호 PFN)에 적용한 것이다.

  • CPU 안에 아주 작은 표(보통 수십~수백 개 항목, 여기서는 이해를 돕기 위해 4칸으로 축소)를 두고 최근 변환 결과를 저장한다.
  • 같은 페이지에 다시 접근하면 페이지 테이블을 아예 안 보고 TLB에서 1사이클 만에 PFN을 얻는다 → 메모리 접근이 2회에서 1회로 줄어든다.
  • TLB에 없으면(TLB 미스) 어쩔 수 없이 메모리의 페이지 테이블까지 가서(느림) 찾아온 뒤, 다음을 위해 TLB에 채워 넣는다. 자리가 없으면 캐시처럼 오래된 항목을 축출한다.
즉: TLB = "주소 변환 전용, 완전 연관(fully-associative) 방식의 아주 작은 캐시". 캐시 매핑 페이지에서 완전 연관 방식이 "하드웨어가 비싸서 TLB처럼 작은 캐시에만 쓴다"고 했던 게 바로 이 얘기다.
VPN(가상 페이지 번호) 4비트 — 16개 페이지 offset 4비트 — 페이지 크기 16B

아래 시뮬레이터에서는 이 축소 모델(8비트 가상주소)을 쓴다. TLB가 캐싱하는 건 VPN 부분뿐 — offset은 원래 그대로 붙는다.

3. 주소 변환 시뮬레이터 — TLB 히트 vs 미스

가상 페이지 번호(VPN)를 하나씩 조회한다. TLB 히트면 초록으로 즉시 끝나고, TLB 미스면 빨간색으로 표시된 뒤 아래 페이지 테이블까지 가서 찾아오고 TLB에 새로 채운다(4칸이 꽉 차면 가장 오래된 항목 축출, LRU).

직접 조회 → VPN
조회 횟수
0
TLB 히트
0
TLB 미스
0
히트율
–

TLB (4칸, LRU 축출)

페이지 테이블 (메모리 안, 16개 페이지 전부)

관찰 포인트: "반복 접근"은 같은 페이지 4개를 계속 오가므로 TLB 4칸에 전부 들어가고도 남아 워밍업 이후 히트율이 거의 100%가 된다. "무작위 접근"은 16개 페이지 중 아무거나 골라 매번 다른 곳을 건드리니 TLB가 매번 비워지고 채워지길 반복해 히트율이 낮다. 이건 지역성의 원리가 캐시뿐 아니라 TLB에도 그대로 적용된다는 뜻이다.

4. TLB 미스의 진짜 비용 — 다단계 페이지 테이블

지금까지는 페이지 테이블이 한 단계(1-level)라서 TLB 미스가 나도 메모리 접근이 딱 1번만 추가됐다. 그런데 64비트 주소공간에서는 페이지 테이블 하나를 통짜 배열로 두면 크기가 감당이 안 돼서, 실제로는 여러 단계의 트리로 쪼갠다(가상 메모리 & 페이징 — 다단계 페이지 테이블 참고). 문제는 TLB 미스가 나면 이 트리를 뿌리부터 끝까지 전부 훑어야 한다는 것.

구조TLB 미스 시 추가 메모리 접근
1단계 페이지 테이블(이 페이지의 시뮬레이터)1회
x86 32비트, 2단계 페이징최대 2회
x86-64, 4단계 페이징(PML4 → PDPT → PD → PT)최대 4회
5단계 페이징(최신 대용량 주소공간)최대 5회
즉: 이 "페이지 테이블 walk"을 한 번 하는 비용이 단계 수만큼 곱해진다. TLB 히트가 왜 그렇게 소중한지 여기서 드러난다 — 미스 하나가 데이터 접근 1회를 4~5회의 추가 메모리 접근으로 불려버릴 수 있다.

5. 컨텍스트 스위칭과 TLB — ASID

TLB에 저장된 (VPN → PFN) 매핑은 특정 프로세스의 가상 주소공간을 전제로 한다. 그런데 OS가 다른 프로세스로 컨텍스트 스위칭을 하면 가상 주소공간 자체가 바뀐다 — 프로세스 A의 VPN 3과 프로세스 B의 VPN 3은 완전히 다른 물리 페이지를 가리킨다.

가장 단순한 방법은 프로세스가 바뀔 때마다 TLB를 통째로 비우는 것(flush)이다. 안전하지만, 새 프로세스가 실행을 재개한 직후엔 TLB가 텅 비어 있어 한동안 미스가 쏟아진다(콜드 스타트).

이를 완화하려고 실제 CPU들은 TLB 항목에 ASID(Address Space ID)라는 프로세스 식별자를 같이 저장한다. 그러면 컨텍스트 스위칭이 일어나도 TLB를 비우지 않고, "이 항목은 프로세스 7 것"이라는 태그로 구분해 두었다가 필요한 것만 골라 쓸 수 있다.

정리: flush = 확실하지만 느림, ASID = 태그로 구분해서 flush를 피하는 하드웨어 최적화.

6. 코드로 보면

TLB를 아주 단순화하면 결국 고정 용량 dict + LRU 축출이다. 캐시 매핑 페이지의 LRU 축출과 개념이 동일하다.

# 아주 단순화한 TLB 흉내: dict + LRU 축출
from collections import OrderedDict

class TLB:
    def __init__(self, capacity=4):
        self.capacity = capacity
        self.table = OrderedDict()          # vpn -> pfn

    def access(self, vpn, page_table):
        if vpn in self.table:
            self.table.move_to_end(vpn)     # 방금 썼으니 MRU로 갱신
            return self.table[vpn], "HIT"       # 메모리 접근 1회로 끝

        pfn = page_table[vpn]               # TLB 미스 → 페이지 테이블 walk (메모리 접근 +1회)
        if len(self.table) >= self.capacity:
            self.table.popitem(last=False)   # 가장 오래 안 쓴 항목 축출
        self.table[vpn] = pfn
        return pfn, "MISS"

위 시뮬레이터가 하는 일이 정확히 이거다 — table이 TLB 4칸, page_table이 메모리 위의 진짜 페이지 테이블 16칸.

관련 페이지

한 줄 정리: TLB는 페이지 테이블 조회 결과를 캐싱하는 초고속 캐시다 — "자주 쓰는 걸 가까이 둔다"는 캐시의 원리를, 데이터가 아니라 주소 변환에 적용했을 뿐이다.