Memory: Cache
캐시 메모리 (Cache Memory)
캐시 메모리는 컴퓨터 시스템의 성능을 향상시키기 위해 CPU와 주 기억 장치(메인 메모리) 사이에 위치하는 작고 빠른 버퍼 메모리입니다. 프로세서가 메인 메모리에서 데이터를 가져오는 속도와 프로세서가 데이터를 처리하는 속도 간의 차이가 크기 때문에, 이 병목 현상을 완화하고 프로세서가 필요한 데이터에 더 빠르게 접근할 수 있도록 돕는 것이 캐시 메모리의 주요 목적입니다.
1. 캐시의 기본 원리: 지역성 (Locality)
캐시 메모리가 효율적으로 작동하는 핵심적인 이유는 '지역성의 원리' 때문입니다. 프로그램의 메모리 접근 패턴은 무작위적이지 않고 특정 경향을 보인다는 관찰에서 출발합니다.
시간 지역성 (Temporal Locality): 한 번 접근했던 데이터는 가까운 미래에 다시 접근될 가능성이 높습니다. 예를 들어, 루프(loop) 안의 변수나 함수 호출 시 반복적으로 사용되는 데이터 등이 이에 해당합니다.
공간 지역성 (Spatial Locality): 특정 데이터에 접근했을 때, 그 데이터의 주변(가까운 주소)에 있는 데이터들도 가까운 미래에 접근될 가능성이 높다는 원리입니다. 배열(array)의 순차적 접근이나 명령어의 순차적 실행 등이 좋은 예시입니다.
2. 캐시 메모리의 구성 및 특징
위치: 일반적으로 CPU 칩 내부(L1 캐시) 또는 CPU와 가까운 곳(L2, L3 캐시)에 위치합니다.
재료: 주로 SRAM(Static Random Access Memory)으로 구성됩니다. SRAM은 플립플롭(Flip-flop) 소자를 사용하여 전원이 공급되는 동안 데이터를 유지하며, 커패시터를 사용하지 않아 재충전이 필요 없고 DRAM보다 훨씬 빠릅니다.
용량: 프로세서에 가장 가까운 메모리이므로, 다른 메모리 계층 요소들보다 용량이 작고 비트당 비용이 가장 높습니다.
3. 캐시 메모리 작동 방식의 핵심 용어
CPU가 데이터에 접근할 때 캐시 메모리에서 해당 데이터를 찾아보고, 그 결과에 따라 다음과 같은 용어들이 사용됩니다.
블록 (Block): 캐시와 메인 메모리 간에 데이터를 복사하는 최소 단위입니다.
히트 (Hit): CPU가 요청한 데이터가 캐시 메모리 안에 이미 존재하는 경우를 말합니다.
히트율 (Hit Rate / Hit Ratio): 전체 메모리 접근 시도 중 캐시 히트가 발생한 비율입니다. 이 비율이 높을수록 캐시의 성능이 좋다고 평가됩니다. 캐시 히트율=Number of cache hits+Number of cache missesNumber of cache hits
히트 타임 (Hit Time): 캐시에서 데이터를 성공적으로 찾아 가져오는 데 걸리는 시간입니다.
미스 (Miss): CPU가 요청한 데이터가 캐시 메모리 안에 존재하지 않는 경우를 말합니다. 이 경우, 캐시는 하위 레벨(메인 메모리)에서 해당 블록을 가져와 캐시에 저장한 후 CPU에 전달합니다.
미스율 (Miss Rate / Miss Ratio): 전체 메모리 접근 시도 중 캐시 미스가 발생한 비율입니다.
미스율 = 1 - 히트율
입니다.미스 패널티 (Miss Penalty): 캐시 미스가 발생했을 때, 하위 레벨 메모리에서 필요한 블록을 가져와 캐시를 업데이트하고, 해당 블록을 프로세서에 전달하는 데 걸리는 총 시간입니다. 미스 패널티는 캐시 성능에 매우 큰 영향을 미칩니다.
캐시를 잘 사용하기 위해 고려할 점
데이터가 캐시에 있는지는 어떻게 판단할 수 있을까?
유효 비트(Valid Bit) 확인:
인덱스로 찾아낸 캐시 블록(또는 세트 내 블록)에 할당된 유효 비트(Valid Bit)를 확인합니다.
유효 비트가 '0'이면, 해당 캐시 엔트리에는 유효한 데이터가 없다는 의미입니다 (예: 컴퓨터 전원을 켰을 때 초기 상태). 이 경우 캐시 미스(Cache Miss)가 발생합니다.
유효 비트가 '1'이면, 해당 캐시 엔트리에 유효한 데이터가 있을 가능성이 있으므로 다음 단계로 넘어갑니다.
캐시에 있다면, 어디에 있는지 어떻게 찾을 수 있을까?
태그(Tag) 비교:
요청된 메모리 주소의 태그 부분을 추출합니다.
인덱스로 찾아낸 캐시 블록(또는 세트 내 블록)에 저장된 태그(Tag) 값과 요청 주소의 태그 값을 비교합니다.
두 태그 값이 일치하면 요청한 데이터가 캐시에 있음을 의미하며, '캐시 히트(Cache Hit)'입니다.
두 태그 값이 일치하지 않으면 비록 유효 비트가 1이라도 요청한 데이터가 아니므로, '캐시 미스(Cache Miss)'가 발생합니다. (이 캐시 엔트리에는 이전에 다른 메모리 블록이 로드되어 있었고, 이제 요청된 블록이 그 자리를 대체해야 한다는 의미입니다.)
예시
메모리 - 캐시 매핑(mapping)
블록 크기가 1워드 이상일 때 메모리 주소가 캐시에서 어떻게 사용되는지를 보여줍니다. 특히, 메모리 주소의 비트들이 태그(Tag), 캐시 인덱스(Cache Size), 블록 오프셋(Block Offset), 바이트 오프셋(Offset)으로 나뉘는 구조를 설명하고 있습니다.
32비트 메모리 주소를 기준으로 했을 때, 주소는 크게 네 부분으로 나뉩니다:
태그 (Tag): 32−n−m−2 비트로 구성됩니다.
이 부분은 캐시 블록에 저장된 데이터가 메인 메모리의 어느 블록에서 왔는지를 식별하는 데 사용됩니다. CPU가 요청한 데이터가 캐시에 있는지 판단할 때, 해당 캐시 라인의 태그 값과 요청 주소의 태그 값을 비교합니다.
캐시 인덱스 (Cache Size): n 비트로 구성됩니다.
이 n 비트는 캐시 내의 블록 개수(2^n개)를 나타냅니다. 직접 매핑 캐시나 세트 연관 캐시에서 요청된 메모리 주소의 데이터가 캐시 내의 어느 특정 위치(블록 번호) 또는 세트(Set)에 매핑될지를 결정하는 데 사용됩니다.
블록 오프셋 (Block Offset): m 비트로 구성됩니다.
블록 크기가 2^m 워드(2^{m+2} 바이트)인 경우, 이 m 비트는 해당 블록 내에서 원하는 워드(word)를 선택하는 데 사용됩니다. 즉, 블록 내에서 몇 번째 워드인지를 나타냅니다.
바이트 오프셋 (Offset): 2비트로 구성됩니다.
워드(word) 내에서 원하는 특정 바이트(byte)를 선택하는 데 사용됩니다. 1워드가 4바이트인 경우, 2비트(2^2 = 4)가 필요합니다.
캐시 메모리와의 관계
2^n
: 이는 캐시 내의 전체 블록 개수를 나타냅니다. 'Cache Index' 부분이 n 비트이므로 2^n개의 인덱스 가능한 위치가 있음을 의미합니다.Valid, Tag, Block (2^m words): 캐시 메모리의 각 엔트리(행)는 유효 비트(Valid), 태그(Tag), 그리고 실제 데이터 블록으로 구성됩니다. 데이터 블록의 크기는 2m 워드입니다.
Valid (유효 비트): 해당 캐시 엔트리에 유효한 데이터가 저장되어 있는지를 나타냅니다.
Tag (태그): 메인 메모리 주소의 태그 부분과 비교하여 캐시 히트 여부를 최종적으로 판단하는 데 사용됩니다.
Block (2^m words): 메인 메모리에서 가져온 실제 데이터 블록입니다. 블록 크기가 2^m 워드라는 것은 공간 지역성을 활용하여 주변 데이터를 함께 가져옴으로써 미스율을 낮추는 데 기여한다는 주석이 있습니다. 이는 "뭉탱이로 가져와서 그 안에서 선택하는 방식 (캐시에 묶음으로 가져옴) 주변을 불러올 가능성이 높아 miss rate 가 낮아진다"는 설명으로 뒷받침됩니다.
총 캐시 비트 개수
캐시에서 비트의 총 개수는 데이터 저장에 필요한 비트의 약 1.15배를 곱한 개수입니다.
캐시 미스의 주요 원인 (The Three Cs of Cache Misses)
모든 캐시 미스는 크게 세 가지 범주로 분류됩니다.
강제 미스 (Compulsory Misses / Cold-Start Misses):
프로그램 실행 중 특정 블록에 처음 접근할 때 발생하는 미스입니다. 캐시가 비어있거나 이전에 한 번도 로드되지 않은 블록에 접근할 때 발생합니다. 캐시 크기나 연관도와는 무관하게 필연적으로 발생합니다.
용량 미스 (Capacity Misses):
캐시의 크기가 충분하지 않아 프로그램 실행에 필요한 모든 블록을 담을 수 없을 때 발생합니다. 캐시가 너무 작아서 이전에 로드했던 블록이 다른 블록에 의해 밀려나고 나중에 다시 필요할 때 발생합니다. 캐시 크기를 늘림으로써 줄일 수 있습니다.
충돌 미스 (Conflict Misses / Collision Misses):
직접 매핑 또는 세트 연관 캐시에서 발생하며, 여러 메인 메모리 블록이 동일한 캐시 세트(또는 블록)에 매핑되어 서로를 밀어낼 때 발생합니다. 캐시에 공간이 남아있음에도 불구하고 매핑 제약으로 인해 발생하는 미스입니다. 연관도(Associativity)를 높임으로써 줄일 수 있습니다. 완전 연관 캐시에서는 충돌 미스가 발생하지 않습니다.
Miss Rate vs. Block Size
고정된 캐시 크기로 가정합니다.
블록 크기가 너무 커지면 블록 A를 가져왔을 때 캐시 대부분을 블록 하나가 채우게 됩니다.
블록 B를 가져오려면 캐시에 있는 블록 A를 버리고 B를 가져와야 하기 때문에 miss rate가 증가하게 됩니다.
6. 캐시의 Miss handling
원래 PC 값(주소)을 메모리로 보냅니다.
캐시 미스가 발생하면, CPU가 요청했던 데이터의 원래 주소(PC 값)를 메인 메모리(주기억장치)로 전달하여 해당 데이터를 찾아오도록 합니다.
주 메모리에 읽기 작업을 지시하고 메모리 접근 완료를 기다립니다.
주 메모리 컨트롤러는 전달받은 주소에 해당하는 데이터를 읽기 시작하고, 이 작업이 완료될 때까지 시스템은 기다리게 됩니다. 이 과정은 캐시 접근보다 훨씬 느립니다.
데이터를 캐시로 가져와 저장합니다.
주 메모리에서 읽어온 데이터를 캐시 엔트리의 데이터 부분에 저장합니다.
동시에, 주소의 상위 비트(Tag)를 캐시 엔트리의 태그 필드에 기록합니다.
해당 캐시 엔트리의 유효 비트(Valid Bit)를 '1'로 설정하여, 이제 이 캐시 블록에 유효한 데이터가 저장되었음을 표시합니다.
명령어 실행을 첫 단계부터 다시 시작하여 캐시에서 명령어를 다시 가져옵니다.
캐시 미스가 발생했던 바로 그 명령어를 다시 한 번 실행하려고 시도합니다.
이번에는 데이터가 캐시에 있으므로(히트), 정상적으로 명령어 실행을 재개할 수 있습니다.
7. 캐시의 데이터 쓰기 정책 (Write Policy)
CPU가 캐시에 데이터를 쓸 때, 캐시와 하위 레벨 메모리(주로 메인 메모리) 간의 데이터 일관성(consistency)을 유지하는 방법입니다.
쓰루 (Write-Through):
데이터를 캐시와 하위 레벨 메모리에 동시에 씁니다.
메모리 간 데이터 일관성을 항상 유지하기 쉽다는 장점이 있지만, 하위 레벨 메모리의 쓰기 속도가 느리므로 전체 성능이 저하될 수 있습니다. 이 성능 저하를 완화하기 위해 쓰기 버퍼(Write Buffer)를 사용하여 CPU가 쓰기 작업이 완료되기를 기다리지 않고 다음 작업을 계속할 수 있도록 합니다.
Write Buffer 가 모두 차 있는 경우에는 stall 이 발생합니다.
백 (Write-Back):
데이터를 일단 캐시 블록에만 씁니다.
수정된 블록(Dirty Block)은 '더티 비트(Dirty Bit)'를 통해 표시되고 , 해당 블록이 캐시에서 교체될 때에만 하위 레벨 메모리로 다시 쓰여집니다.
잦은 쓰기 작업이 발생할 경우, 여러 번의 캐시 쓰기가 단 한 번의 하위 레벨 쓰기로 이어질 수 있어 메모리 접근 효율을 크게 향상시킵니다. 주 메모리와의 일관성 유지에는 더 복잡한 제어 로직이 필요합니다. 가상 메모리 시스템에서는 쓰기 백 정책이 주로 사용됩니다.
8. 캐시 교체 정책 (Replacement Policy)
세트 연관 캐시나 완전 연관 캐시에서 미스가 발생하고 해당 세트(또는 캐시 전체)가 가득 찬 경우, 새로운 블록을 위해 기존 블록 중 하나를 제거해야 합니다.
LRU (Least Recently Used): 가장 오랫동안 사용되지 않은 블록을 교체하는 정책입니다. 지역성의 원리를 잘 활용하여 미스율을 낮추는 데 효과적이지만, 구현 비용이 높습니다. 특히 연관도(N)가 커질수록 LRU를 정확히 구현하기는 매우 어렵습니다.
Random (무작위): 교체할 블록을 무작위로 선택합니다. 구현이 간단하지만, LRU만큼 효율적이지는 않습니다.
9. 다중 레벨 캐시 (Multilevel Caches)
최신 프로세서들은 CPU와 메인 메모리 사이의 성능 격차를 더욱 줄이기 위해 여러 단계의 캐시를 사용합니다.
L1 캐시 (Level 1 Cache): CPU 코어에 가장 가깝고 가장 빠르며 용량이 가장 작습니다. 명령어 캐시(L1I)와 데이터 캐시(L1D)로 분리되는 경우가 많습니다.
L2 캐시 (Level 2 Cache): L1 캐시보다는 느리지만 용량이 더 크고, L1 미스 발생 시 접근됩니다.
L3 캐시 (Level 3 Cache): L2 캐시보다 더 느리지만 용량이 가장 크며, 여러 CPU 코어 간에 공유될 수 있습니다.
멀티레벨 캐시의 장점:
성능 향상: CPU가 L1 캐시에서 데이터를 찾지 못하더라도, L2 또는 L3 캐시에서 데이터를 찾을 가능성이 높아집니다. 이렇게 되면 주 메모리까지 접근하는 데 드는 막대한 시간(미스 페널티)을 줄일 수 있습니다.
미스 페널티 감소: L1 캐시 미스 시 L2 캐시에서 데이터를 가져오는 데 걸리는 시간(예: 5 ns)은 주 메모리에서 가져오는 시간(예: 100 ns)보다 훨씬 짧습니다. 이는 전체 시스템 성능에 큰 이점을 가져다줍니다.
성능 계산 예시:
기본 가정:
CPU 기본 CPI (Cycles Per Instruction): 1.0 (모든 참조가 L1 캐시에서 히트할 때)
클럭 속도: 4 GHz (0.25 ns/클럭 사이클)
L1 캐시 미스율: 명령어당 2%
주 메모리 접근 시간: 100 ns
L2 캐시 접근 시간: 5 ns
L2 캐시 미스율 (주 메모리까지): 0.5% (L1 미스가 L2에서도 미스할 확률)
미스 페널티 계산:
주 메모리 미스 페널티: 100 ns/0.25 ns/클럭 사이클=400 클럭 사이클
L2 캐시 미스 페널티 (L1 미스 시 L2에서 가져올 때): 5 ns/0.25 ns/클럭 사이클=20 클럭 사이클
단일 L1 캐시만 있을 때의 총 CPI:
Total CPI = Base CPI + 명령어당 메모리-스톨 사이클
Total CPI = 1.0+(400 클럭 사이클×0.02)=1.0+8=9.0
L2 캐시가 추가되었을 때의 총 CPI:
Total CPI = Base CPI + 명령어당 메모리-스톨 사이클 (L1 + L2)
Total CPI = 1.0+(20 클럭 사이클×0.02)+(400 클럭 사이클×0.005)=1.0+0.4+2=3.4
성능 향상:
L2 캐시를 추가함으로써 프로세서는 9.0/3.4≈2.6 배 더 빨라집니다.