변환 색인 버퍼
더 많은 작업
- Translation Lookaside Buffer, TLB; 변환 색인 버퍼, 변환 참조 버퍼
- 최근에 쓴 가상 주소와 물리 주소의 대응을 담아 두어 페이지 테이블을 읽지 않고 주소를 바꾸게 해 주는 캐시
변환 색인 버퍼(TLB)는 주소 변환 전용 캐시다. 가상 메모리를 쓰는 시스템은 메모리에 접근할 때마다 가상 주소를 물리 주소로 바꿔야 하는데, 변환 정보가 담긴 페이지 테이블도 주기억장치에 있다. 그대로라면 메모리 접근 한 번에 페이지 테이블 접근이 더 붙고, 다단계 페이지 테이블이면 네다섯 번이 더 붙는다. TLB는 자주 쓰는 변환 결과를 CPU 안의 작은 고속 버퍼에 담아 이 비용을 거의 없앤다.
TLB는 메모리 관리 장치(MMU)의 일부이며, 페이징이나 세그먼트 방식 가상 메모리를 쓰는 프로세서에는 거의 항상 있다. 명령어 파이프라인 안에서 매 접근마다 찾아야 하므로 작고 빨라야 하고, 그래서 항목 수가 수십~수천 개로 적다. 참조 지역성 덕분에 이 정도로도 적중률이 보통 99% 이상이다.
- 1단계: CPU가 가상 주소를 내면 가상 페이지 번호(VPN)로 TLB를 찾는다.
- 2단계, TLB 적중(hit): 물리 프레임 번호(PFN)를 바로 얻어 페이지 안 오프셋과 합쳐 물리 주소를 만든다. 권한 비트도 이때 검사한다.
- 2단계, TLB 부적중(miss): 페이지 테이블을 걸어(page walk) 해당 항목을 찾고, 그 항목을 TLB에 채운 뒤 접근을 다시 한다. 하드웨어가 걷는 방식과 운영체제가 걷는 방식이 있다(아래 절).
- 3단계: 페이지 테이블 항목도 무효(유효 비트 0)면 페이지 부재(page fault)가 일어나 운영체제가 페이지를 메모리에 올리거나 프로세스에 오류를 보낸다.
TLB 미스는 명령어나 데이터 캐시 미스보다 비쌀 수 있다. 데이터가 캐시에 있어도 변환 정보가 TLB에 없으면 페이지 테이블 여러 단계를 메모리에서 읽어야 하기 때문이다.
- 연관 메모리(내용 주소화 기억장치, CAM)나 집합 연관 구조로 만들어 여러 항목을 동시에 비교한다. 완전 연관은 빠르고 충돌이 없지만 비싸서 작은 L1 TLB에 쓰고, 큰 TLB는 4~12방향 집합 연관으로 만든다.
- 한 항목은 태그(가상 페이지 번호)와 값(물리 프레임 번호)의 쌍이며, 유효 비트, 읽기·쓰기·실행·사용자 권한 비트, 더티 비트, 캐시 속성, 주소 공간 식별자(ASID/PCID), 전역(G) 비트, 페이지 크기를 함께 둔다.
- 명령어용 ITLB와 데이터용 DTLB를 따로 두는 경우가 많다(하버드 구조). 여기에 둘이 함께 쓰는 L2 TLB를 두기도 한다.
- 캐시와의 관계: 물리 주소로 색인하는 캐시라면 캐시 접근 전에 TLB를 거쳐야 한다. 그래서 L1 캐시는 가상 주소로 색인하고 물리 주소로 태그를 비교하는 방식(VIPT)을 써서 TLB 조회와 캐시 조회를 동시에 한다. 가상 주소로 색인·태그하는 캐시라면 캐시 미스 때만 TLB를 본다.
캐시처럼 TLB도 여러 단계를 둔다. 작고 아주 빠른 L1 ITLB와 L1 DTLB, 그 뒤에 조금 느리지만 큰 공유 L2 TLB(인텔 명칭 STLB, Second-level TLB)가 흔한 구성이다. 페이지 크기별로 항목을 따로 두기도 한다.
인텔 네할렘(2008년)의 예는 다음과 같다.[1]
| TLB | 4KB 페이지 | 2MB/4MB 페이지 | 구성 |
|---|---|---|---|
| L1 DTLB | 64항목 | 32항목 | 4방향 집합 연관 |
| L1 ITLB | 128항목 | 7항목(스레드마다) | 4방향 / 완전 연관 |
| L2 TLB (STLB) | 512항목 | 없음 | 4방향, 명령어와 데이터 공유 |
이후 세대는 L2 TLB가 수천 항목으로 커지고 거대 페이지 항목도 L2에 담는다. 스카이레이크는 1GB 페이지용 항목을 4KB/2MB용과 따로 둔다.[2]
교과서가 드는 일반적인 수치는 다음과 같다.[3]
- 크기: 16~512항목(더 큰 L2 TLB는 수천 항목)
- 적중 시간: 0.5~1클록
- 미스 비용: 10~100클록
- 미스율: 0.01~1%(그래프 처리처럼 접근이 흩어진 작업은 훨씬 높음)
TLB가 가득 차면 새 항목을 넣을 자리를 비워야 한다. 하드웨어 관리 TLB는 LRU 근사(의사 LRU), FIFO, 무작위 교체를 쓴다. 소프트웨어 관리 TLB(MIPS)는 운영체제가 어느 칸에 쓸지 고를 수 있어서 보통 무작위 칸(tlbwr)에 쓰고, 커널용 항목은 교체되지 않는 고정 칸(tlbwi로 쓰는 wired 영역)에 둔다.
TLB 접근 시간을 t, 메모리 접근 시간을 m, 적중률을 h라 하고 페이지 테이블이 한 단계라고 하면 다음과 같다.
- 적중: t + m
- 부적중: t + 2m (페이지 테이블 1번, 실제 데이터 1번)
- 유효 접근 시간(EAT) = h(t + m) + (1 - h)(t + 2m) = t + m + (1 - h)m
예를 들어 t = 20ns, m = 100ns, h = 0.98이면 EAT = 0.98 × 120 + 0.02 × 220 = 117.6 + 4.4 = 122ns다. TLB가 없으면 매번 200ns이므로 약 39% 빨라진다. TLB 조회를 캐시 접근과 동시에 하는 구조라면 t를 0으로 보고 계산하기도 한다.
k단계 페이지 테이블이면 부적중 비용이 t + (k + 1)m이 된다. 4단계라면 부적중 한 번에 메모리를 다섯 번 읽으므로 적중률이 조금만 떨어져도 성능이 크게 나빠진다. 실제 CPU는 중간 단계 항목을 따로 캐시(페이징 구조 캐시)하고 페이지 테이블 항목도 데이터 캐시에 올라가 있어서 이보다 덜 나빠진다.
클록 수로 쓰기도 한다. 메모리 읽기 m클록, 적중 시간 h클록, 미스율 p일 때 평균은 m + (1 - p)h + pm클록이다. 적중 1클록, 미스 30클록, 메모리 읽기 30클록, 미스율 1%라면 30 + 0.99 × 1 + 0.01 × 30 = 31.29클록이다.
| 방식 | 누가 페이지 테이블을 걷나 | 페이지 테이블 형식 | 예 |
|---|---|---|---|
| 하드웨어 관리 | CPU의 페이지 워커. x86은 CR3가 가리키는 표부터 걷는다 | 아키텍처가 고정 | x86, ARM, RISC-V(Sv39 등), PowerPC 일부 |
| 소프트웨어 관리 | TLB 미스 예외를 받은 운영체제 코드 | 운영체제 마음대로(해시 표, 선형 표 등) | MIPS, UltraSPARC Architecture 2005, 초기 PowerPC(G1) |
| 펌웨어 관리 | 펌웨어(PALcode) | 운영체제별 PALcode가 정함 | DEC 알파 |
| 선택 가능 | 둘 다 | 둘 다 | 이타늄, SPARC V9(구현이 선택) |
- 하드웨어 관리: 미스가 나도 예외 없이 하드웨어가 표를 걷고 TLB를 채운다. 유효한 항목이 없을 때만 페이지 폴트를 낸다. TLB 항목 형식이 소프트웨어에 보이지 않으므로 CPU 세대마다 TLB를 바꿔도 운영체제는 그대로다. 대신 페이지 테이블 형식이 아키텍처에 고정된다. 여러 미스를 동시에 처리하는 페이지 워커를 여러 개 두기도 한다.
- 소프트웨어 관리: MIPS는 TLB에 맞는 항목이 없으면 TLB 재적재(refill) 예외를, 항목은 있는데 무효면 TLB 무효 예외를, 쓰기인데 더티 비트가 0이면 TLB 수정 예외를 낸다. 운영체제는 짧은 처리기에서 표를 찾아
tlbwr같은 명령으로 TLB에 직접 써 넣는다. 페이지 테이블 구조를 운영체제가 자유롭게 고를 수 있고 하드웨어가 단순해지지만, 미스마다 예외 진입 비용이 든다. MIPS의 TLB 항목 하나는 이웃한 가상 페이지 두 개(짝수, 홀수)를 한꺼번에 담고, 항목마다 페이지 크기를 따로 정할 수 있다. - 알파는 미스 처리 코드를 운영체제가 아닌 PALcode에 두어, 같은 CPU에서 오픈VMS, OSF/1, 윈도우 NT가 서로 다른 페이지 테이블 형식을 썼다.
주소 공간이 바뀌면(프로세스 사이 문맥 교환, 스레드 사이는 해당 없음) 이전 프로세스의 변환은 새 프로세스에 틀린 값이다. 가장 단순한 방법은 TLB를 모두 비우는(flush) 것이다. 하지만 그러면 전환 직후 모든 접근이 미스가 되어 한동안 느려진다.
TLB를 비우지 않는 방법은 다음과 같다.
- 주소 공간 식별자: 항목마다 어느 주소 공간의 것인지 태그를 붙이고, 현재 식별자와 같은 항목만 쓴다. 알파의 ASN, MIPS와 ARM의 ASID(ARMv8은 8비트 또는 16비트), x86의 PCID가 이것이다.
- 전역 페이지: 모든 프로세스가 같은 매핑을 쓰는 커널 영역은 전역(G) 비트를 붙여 주소 공간을 바꿔도 남긴다. x86은 펜티엄 프로부터 CR4.PGE로 지원한다.
- 단일 주소 공간 운영체제: 모든 프로세스가 같은 가상 주소 공간을 나눠 써서 변환 자체가 바뀌지 않게 한다.
x86의 PCID(Process-Context Identifier)는 12비트(4096개)이며 2010년 웨스트미어부터 지원한다.[4] 프로세스가 4096개보다 훨씬 많으므로 리눅스는 4.14부터 PCID를 프로세스마다 고정하지 않고, CPU마다 최근에 쓴 주소 공간 몇 개에 번호를 돌려 쓰는 방식으로 활용한다.[5]
운영체제가 페이지 테이블 항목을 바꾸거나 지우면(munmap, mprotect, 페이지 교체, 쓰기 시 복사) 그 변환을 TLB에서도 지워야 한다. 하드웨어는 페이지 테이블이 바뀐 것을 스스로 알지 못하기 때문이다.
- 선택적 무효화: 80386은 CR3를 다시 써서 전체를 비우는 방법밖에 없었지만, 80486부터
INVLPG로 주소 하나의 항목만 지운다.INVPCID는 특정 PCID의 항목을 지운다. ARM은TLBI명령에 주소, ASID, VMID 범위를 지정한다. - TLB 슈트다운(TLB shootdown): 다중 프로세서에서는 같은 주소 공간을 쓰는 다른 코어의 TLB에도 옛 항목이 남아 있을 수 있다. x86에서는 변경한 코어가 해당 코어들에 프로세서 간 인터럽트(IPI)를 보내 각자 무효화하게 하고, 모두 끝났다는 응답을 기다린다. 이것을 TLB 슈트다운이라 한다. 스레드가 많은 프로그램이
munmap을 자주 하면 IPI와 대기 비용이 커져 확장성이 떨어진다. ARM과 파워는 무효화 명령 자체가 인터커넥트로 다른 코어에 전파(broadcast)되어 IPI가 필요 없다. AMD는 최근 x86에도 방송형 무효화 명령(INVLPGB)을 넣었다. - 슈트다운 비용을 줄이려고 리눅스는 무효화를 모아서 한 번에 하고(batching), 그 주소 공간을 최근에 쓴 코어에만 IPI를 보낸다.
TLB 도달 범위(TLB reach)는 TLB가 한 번에 덮을 수 있는 메모리 양으로, 항목 수 × 페이지 크기다. 4KB 페이지에 항목이 1,536개면 도달 범위는 6MB뿐이라, 수십 GB를 흩어서 읽는 데이터베이스나 JVM 힙, 과학 계산은 TLB 미스가 잦다. 작업 집합이 TLB 도달 범위를 넘어 미스가 폭증하는 상태를 TLB 스래싱이라 한다. 코드와 데이터가 캐시에는 다 들어가도 여러 페이지에 흩어져 있으면 TLB 스래싱이 날 수 있다.
거대 페이지(huge page)를 쓰면 항목 하나가 2MB나 1GB를 덮는다. 같은 항목 수로 도달 범위가 512배, 262,144배가 되고, 페이지 워크도 한두 단계 짧아진다.
- 리눅스 hugetlbfs: 부팅 때나 실행 중에 거대 페이지 풀을 예약하고,
mmap의MAP_HUGETLB나 hugetlbfs 파일로 쓴다. 오라클 SGA, PostgreSQL의huge_pages, DPDK가 쓴다. - 투명 거대 페이지(THP): 커널이 알아서 2MB 페이지로 묶는다.
/sys/kernel/mm/transparent_hugepage/enabled에서always,madvise,never를 고른다. 거대 페이지는 TLB 미스를 줄이고 미스 처리도 빠르게 하며, 가상화에서는 게스트와 호스트 모두 거대 페이지를 쓸 때 효과가 가장 크다.[6] 반면 메모리를 묶고 쪼개는 비용, 쓰지 않는 부분까지 잡히는 낭비, 조각 모으기(compaction) 지연 때문에 일부 데이터베이스는 THP를 끄라고 권한다. - 윈도우의 큰 페이지(
MEM_LARGE_PAGES, "메모리에 페이지 잠그기" 권한 필요), JVM의-XX:+UseLargePages도 같은 목적이다.
하드웨어 2단계 변환(인텔 EPT, AMD NPT, ARM Stage 2)을 쓰면 게스트 가상 주소를 게스트 물리 주소로 바꾸는 게스트 페이지 테이블의 각 단계 주소 자체가 게스트 물리 주소라서, 그 하나하나를 다시 호스트 페이지 테이블로 바꿔야 한다. 게스트 n단계, 호스트 m단계면 최악의 경우 메모리 참조가 (n + 1)(m + 1) - 1번이다. 둘 다 4단계면 24번으로, 가상화하지 않은 경우(4번)의 여섯 배다.[7] 그래서 가상화 환경에서는 TLB 적중률과 거대 페이지가 더 중요하다. TLB에는 게스트 가상 주소에서 호스트 물리 주소로 바로 가는 결과가 저장되고, CPU는 중간 단계 변환을 위한 별도 캐시를 둔다. 5단계 페이징이면 최악의 참조가 35번으로 늘어난다.
x86 TLB 항목은 원래 주소 공간을 구분하지 않아, 가상 머신에 들어가고 나올 때마다(VM entry, VM exit) TLB를 모두 비워야 했다. 2008년 인텔(네할렘의 VPID)과 AMD(SVM의 ASID)가 가상 머신 식별자 태그를 TLB 항목에 넣어 이 비움을 없앴다. ARM은 VMID를 쓴다.
멜트다운(2018년 공개)은 추측 실행 중에 사용자 모드 코드가 커널 메모리를 읽고 그 흔적을 캐시로 빼내는 취약점이다. 커널이 모든 프로세스의 주소 공간 위쪽에 매핑되어 있고 권한 검사가 추측 실행을 막지 못한 것이 원인이다. 리눅스의 대응인 커널 페이지 테이블 격리(KPTI, PTI)는 프로세스마다 페이지 테이블을 두 벌 둔다. 커널 모드용은 전체를 담고, 사용자 모드용은 시스템 호출과 인터럽트 진입·복귀에 필요한 최소한의 커널 영역만 담는다. 커널에 들어가고 나올 때마다 CR3를 바꾸는데, 이 쓰기는 백 클록 안팎이 들고, 커널 영역에 전역 페이지도 쓸 수 없게 된다.[8]
PCID가 없으면 CR3를 바꿀 때마다 TLB 전체가 비워져 시스템 호출이 잦은 작업의 성능이 크게 떨어진다. PCID가 있으면 사용자용 표와 커널용 표에 서로 다른 PCID를 주어 전환해도 TLB 항목이 살아남는다. 그래서 KPTI 이후 PCID는 x86에서 성능과 보안 양쪽에 중요한 기능이 되었다. 멜트다운에 취약하지 않은 CPU(AMD, 최근 인텔)에서는 KPTI를 켜지 않는다. nopti나 pti=off 부팅 옵션으로 끌 수도 있다.
- ↑ Inside Nehalem: Intel's Future Processor and System, Real World Technologies (2008)
- ↑ Runtime Performance Optimization Blueprint: Intel Architecture Optimization with Large Code Pages, Intel
- ↑ David A. Patterson, John L. Hennessy, Computer Organization and Design: The Hardware/Software Interface, 4th ed., Morgan Kaufmann, 2009, p. 503
- ↑ Intel 64 and IA-32 Architectures Software Developer's Manual, Vol. 3A, 4.10.1 Process-Context Identifiers (PCIDs)
- ↑ Linux 4.14, Kernel Newbies
- ↑ Transparent Hugepage Support, Linux kernel documentation
- ↑ Ravi Bhargava 외, Accelerating two-dimensional page walks for virtualized systems, ASPLOS 2008
- ↑ Page Table Isolation (PTI), Linux kernel documentation