작지만 빠른 친구 캐시 알아보기
지난 시간 CPU와 메모리 사이의 데이터 교환 방식을 알아보며 이 과정은 비용이 많이 드는 작업임을 알 수 있었습니다. 그 사이에서 효율적인 데이터 사용을 위한 조력자, 캐시에 대해 알아보려 합니다.
캐시 Cache
CPU와 RAM 사이에 위치한 고속 기억 장치로, 데이터 전송 속도를 높이고 시스템 성능을 향상시키는 역할
- 역할: `CPU와 RAM 간의 속도 차이로 발생하는 병목현상을 완화`해 성능을 향상시키는 역할
- 속도: CPU 다음으로 빠른 메모리로, `RAM보다 훨씬 빠른 접근 속도`를 제공합니다.
- 용량: RAM에 비해 `용량이 작지만, 자주 사용되는 데이터를 저장`합니다.
- 위치: CPU와 RAM 사이에 위치하여 `데이터 전송의 중개자 역할`을 합니다.
- 계층 구조: L1, L2, L3 등 여러 단계로 나뉘며, 숫자가 작을수록 CPU에 가깝고 빠르지만 용량이 작습니다.
* 보통 캐시 메모리라고 하면 CPU 내부에 있는 고속 메모리를 뜻합니다.(넓은 의미로는 여러 계층에서 사용 가능한 개념!)
CPU에서 주 기억장치 RAM(혹은 보조 기억장치)에 접근하는 것은 비용이 많이 드는 작업입니다. 이를 용량은 작지만 빠르게 접근 가능한 캐시를 이용해 빠르게 데이터를 전달받아 시스템 성능을 향상시키는 것이 캐시 메모리의 주요 역할입니다.(가격도 훨씬 비싸다고 합니다 💵)
캐시 메모리에 데이터가 저장되는 과정
프로그램이 실행되면 Storage에서 실행 파일을 불러와 RAM에 올리고 CPU는 이를 활용해 연산을 실행합니다. 그렇다면 캐시 메모리에 데이터가 저장되는 순간은 언제일까요?
이전 포스팅과 같이 'x와 y를 더해라!'라는 연산을 실행하는 상황을 가정해보겠습니다.
x값 불러오기 1단계: 데이터 검색
우선 x값을 검색해보겠습니다. x값을 찾기 위해 CPU는 레지스터에서 x값을 찾습니다. 레지스터에 값이 없는 것을 확인하면, 곧바로 첫 번째 캐시 메모리인 L1 캐시에 접근해 x값을 찾습니다. 여기도 없다면 다음 L2, L3까지 검색 후 마지막으로 Memory에서 x값을 발견합니다.
이 경우는 아직 캐시 메모리에 x값이 존재하지 않았던 상황으로 이해할 수 있습니다.
x값 불러오기 2단계: 캐시에 저장 및 CPU에 불러오기
x값을 메모리에서 찾았습니다. 이젠 이를 빠르게 사용하기 위해 캐시에 저장해둡니다. 캐시 저장 기준은 저장 공간이 부족하지 않다면, 가장 빠르게 접근 가능한 순서로 저장하게 됩니다.
즉, L1 캐시에 저장 공간이 남아있다면 L1에 저장하고, 저장 공간이 부족하다면 L2 → L3식으로 말이죠.(이는 캐시 종류에 따라 다르게 동작한다고 합니다. 캐시 교체 정책으로 L1에 가장 오래 사용되지 않은 데이터가 있다면 이를 밀어내고 새로운 데이터를 넣을 수도 있습니다! 하지만 일반적으로는 L1이 우선된다고 생각할 수 있습니다!)
y값 불러오기 1단계: 데이터 검색
이번엔 y값을 검색해보겠습니다. x값을 검색할 때와 같이 Register → L1 → L2 → L3 → Memory(RAM) → Memory(Storage) 순서로 검색하게 됩니다. 하지만 이번에 다른 점은, y값이 이미 L1 캐시에 저장되어 있는 경우였다는 것입니다!
L1 캐시에서 y값을 찾은 직후 더이상 검색을 진행할 필요가 없기 때문에, 곧바로 CPU에 y값을 불러올 수 있게 됩니다.
x와 y를 더해라! 그 이후...
x와 y를 더해라!라는 연산을 CPU에서 잘 수행할 수 있었습니다. 만약 그 이후 'x와 y를 빼라!'라는 연산이 새롭게 들어왔다면 CPU는 데이터를 어떻게 불러올 수 있을까요?
예상할 수 있듯, 이미 L1캐시에 x와 y값 모두 저장되어 있기 때문에 Memory에 접근하지 않고 훨씬 빠르게 데이터를 받아올 수 있게 됩니다!
캐시 적중률, Hit or Miss
위 예시에서 x값 처럼 캐시에서 메모리를 찾지 못한 경우는 Miss(실패), y값 처럼 캐시에서 메모리를 찾은 경우는 Hit(적중)했다고 표현합니다. 그리고 메모리 접근 횟수 중 Hit한 경우의 비율을 캐시 적중률 Cache Hit Rate라고 표현합니다.
당연하게도 캐시 적중률이 높을 수록 CPU의 성능이 향상됩니다. RAM에서 데이터를 찾는 것보다 훨씬 빠르게 처리할 수 있기 때문입니다. 반대로 캐시 적중률이 낮다면 RAM 접근이 많아져 성능이 저하되겠죠?
그리고 위에서 살펴봤듯, L1 캐시에 가장 자주 혹은 많이 사용되는 데이터가 저장되기 때문에 일반적으로 적중률이 가장 높게 계산됩니다. GPT 선생님에 따르면 L1 캐시는 90%이상, L2 캐시는 80~90%, L3 캐시는 50~70%라고 합니다.
캐시 적중률을 높이는 방법
캐시 적중률을 높이는 것은 CPU 성능와 연관이 깊습니다. 캐시 적중률을 높이기 위해 어떤 방법을 사용할 수 있을까요?
- `캐시 크기 증가시키기`: 캐시가 클 수록 더 많은 데이터를 저장할 수 있기 때문에 적중률
- `캐시 교체 정책 최적화`: 가장 오래 / 적게 사용되지 않은 데이터를 제거하거나, 랜덤으로 제거하는 방식으로 적중률 증가
- `데이터 지역성 활용`: 캐시 지역성의 특성을 활용해 적중률을 높이는 방법(아래에서 설명할 것!)
캐시의 지역성 Locality
캐시에서 지역성 원리는 캐시 적중률을 높이는데 중요한 역할을 수행합니다. 지역성은 쉽게 말해, 프로그램의 실행 패턴을 분석하면 특정 데이터를 반복해서 접근하는 경향을 의미합니다. 특정 데이터에 반복해 접근하는 것을 알 수 있다면, 캐시 저장 우선순위를 올려 캐시 적중률을 올릴 수 있고, 결과적으로 CPU 성능을 향상시킬 수 있게됩니다.
1. 시간 지역성 - 같은 데이터 반복 접근
`최근에 접근한 데이터는 가까운 미래에도 다시 접근할 가능성이 있다!`라는 개념입니다. 아래 예시에서 반복문에서 같은 변수를 계속 참조하고 있는 경우 시간 지역성으로 캐시에 남아 있을 가능성이 높습니다.
var sum = 0
for _ in 0..<1000 {
sum += 1 // sum 변수를 계속 사용 (같은 메모리 주소 접근)
}
2. 공간 지역성 - 가까운 메모리 주소 접근
`최근에 접근한 데이터의 주변 데이터에 접근할 가능성이 있다!`라는 개념입니다. 아래 예시에서 배열의 요소를 순차적으로 접근하고 있기 때문에 공간 지역성으로 주변 데이터가 캐시에 남아 있을 가능성이 높습니다.
var array = [Int](repeating: 0, count: 1000)
for i in 0..<1000 {
array[i] = i // 배열의 연속된 메모리 주소 접근
}
위 지역성의 개념을 활용해 자주 사용하는 데이터는 전역 변수 혹은 클래스 변수로 유지해 시간 지역성을 최대한 활용하고, 배열 등을 이용해 공간 지역성을 활용함으로서 캐시 효율성을 최적화할 수 있습니다.
이외에도 순차 지역성, 분기 지역성의 개념이 있지만 위의 두가지 개념이 핵심인 것 같습니다!
정리하기
캐시는 주변에서 꽤나 많이 볼 수 있는 개념이었습니다. 게임을 할 때나, 애플리케이션 저장 공간에도 항상 캐시라는 용어를 찾아볼 수 있었죠. 역시나 그림을 그려가며 하나하나 개념을 따라가다보면 어느새 재밌게 이해하고 있는 제 모습을 발견할 수 있어 좋은 것 같습니다! 😇
Ref.
(CS) 캐시 메모리, 메모리 관리 기법, 페이지 교체 알고리즘
캐시 메모리 1. 캐시 메모리란? 캐시 메모리(Cache Memory)는 CPU와 RAM 간의 데이터 전송 속도를 높이기 위해 사용되는 중앙 처리 장치(CPU)와 주 기억장치(RAM) 사이에 위치한 고속 기억장치다. 캐시 메
lildev.tistory.com