✏️ 메모리 계층(Memory Hierarchy)

💻 메모리 계층 구조

메모리의 구성 => 하드 디스크, 메인 메모리, 캐쉬 (+ 레지스터)

  • 하드 디스크
    • 비휘발성 저장 장치 => 파일 시스템 관점
    • 프로그램의 실행을 담당 => 메모리 매니지먼트 관점
  • 메인 메모리
    • 프로그램의 실행을 담당 (저장 기능 따로 없음)

보통 하드 디스크의 기능을 저장 기능에 초점을 맞춰 배우는데, 현 단원은 메모리 매니지먼트에 관한 것이기 때문에 하드 디스크를 실행 기능의 관점에서 볼 것.
(그러나 메인 메모리의 실행에 대해 설명하려면 하드 디스크의 저장 기능도 필요.)

  • 컴퓨터 디자인 예시로 보는 메모리 계층 구조의 이해

    • CPU와 메모리가 있음.
    • 메모리에서 데이터를 가져다 CPU에서 연산 후, 연산 결과를 메모리에 둠.
    • 이 때 캐쉬라는 연산이 빠른 새로운 메모리 등장.
    • 그러나 기존 50Mb 크기의 메모리를 전부 캐쉬로 교체하려면 돈이 많이 듦. 내가 살 수 있는 캐쉬 메모리는 2Mb뿐.
    • 한편 프로그램 코드를 봤더니 메모리를 산발적으로 여기 저기 접근해서 실행하는게 아니라, 어느 한 부분에 접근했다면 그 주변 가까운 메모리에 접근하는 형태. 즉 지역적인 특성을 지닌다.
    • 따라서 캐쉬를 2Mb만큼 사서 CPU와 메모리 중간에 두고 메모리에서 현재 실행 중 메모리를 캐쉬의 크기만큼 블럭 단위로 나눠서 캐쉬에 올린 후, CPU가 캐쉬를 통해 연산하게 했더니 실제로 성능이 좋아짐.
    • 이는 메모리에서 직접적으로 CPU로 데이터를 보내던 것과 달리 메모리 계층이 있는 구조이고, 계층 구조의 장점을 보여주는 형태.
  • 메모리 구조 간 관계 일치
    하드 디스크 입장에서는 메인 메모리가 캐쉬. 하드 디스크가 데이터를 CPU에 직접적으로 주는게 아니라 메인 메모리에 먼저 올려둠. 즉 모든 메모리는 직접적으로 CPU에 데이터를 넘겨주는게 아니라, 위층 메모리에 데이터를 블럭 단위로 올려둔다.

  • 메모리 구성 간 역할 동일
    메모리(하드 디스크, 메인 메모리, 캐쉬)의 역할은 모두 프로그램 실행을 위해 나보다 계층이 높은 메모리에게 데이터를 밀어 넣는 것.

  • 메모리 계층 구조의 필요성

    • CPU가 메인 메모리에게 데이터 요청.
    • 자신이 가지고 있지 않다면 메인 메모리는 하드 디스크에게 데이터 요청. 하드 디스크가 가지고 있는 데이터라면 이를 메인 메모리에 올려둠
    • 또는 ALU가 자신이 필요한 데이터를 L1 캐쉬에게 요청. L1 캐쉬는 해당 데이터를 가지고 있지 않기 때문에 L1->L2 캐쉬에게 요청. 데이터가 없다면 L2->메인 메모리->하드 디스크 순으로 요청.
    • 하드 디스크가 데이터를 가지고 있음. 그러면 하드 디스크->메인 메모리->L2->L1->레지스터 순으로 데이터 블럭을 밀어 올려두고 가져가는 일을 반복.
    • 프로그램의 실행 시 메모리 접근이 산발적이라면 이처럼 계층 구조 없이 CPU가 하드 디스크에 직접적으로 접근하는 것이 빠를 수 있다. 그러나 프로그램의 메모리 접근 흐름은 지역적인 특성이 강함. 즉 어느 지역에 국한되어서 프로그램이 주로 실행됨. 따라서 메모리는 계층 구조일 때 가장 성능이 좋다.

✏️ 캐시(Cache)와 캐시 알고리즘

지역성(Locality)
프로그램 실행에 필요한 전체 메모리가 있고 메모리의 시작과 끝이 있다고 가정할 때, 프로그램은 그 사이의 메모리를 산발적으로 접근하지 않는다. 한 메모리에 접근했다면 그 다음에는 그 주변의 메모리를 접근하게 될 가능성이 크다. 이처럼 프로그램의 메모리 접근 흐름은 지역성을 지닌다.

세상에 존재하는 모든 프로그램은 지역성을 지닌다. 일부러 메모리의 산발적인 접근 흐름을 만들지 않는 이상 지역성은 프로그램의 근본적인 성격이기 때문에 지역성을 무시할 수 없다.
CPU가 캐쉬에 어떤 데이터를 요청할 시, 캐쉬가 해당 데이터를 가지고 있을 확률은 90%가 넘는다. 이는 지역성의 특징 중 하나. 따라서 캐쉬의 용량 또한 중요하다.

좋은 프로그램이라면 지역성이 좋아서 캐쉬를 원활히 사용할 것. 그러나 평균적으로 지역성이 좋은 프로그램과 좋지 않은 프로그램이 캐쉬를 사용하는 정도의 차이는 그렇게 크지 않다.

💻 프로그램의 일반적 특성

  • 성능 향상과 캐쉬 메모리
    • 캐쉬 메모리가 존재했을 때 성능이 향상되는 이유는 Locality 때문.
      즉 캐쉬 메모리의 유무가 컴퓨터의 성능에 영향을 미친다.
  • Locality의 종류
    • Temporal Locality (반복 접근)
      변수 int A를 선언했다면, 다시 A에 접근한 확률이 높다.
    • Spatial Locality (주변 접근)
      변수 int A와 B를 선언했다면 메모리 공간에 순차적으로 존재할 것.
      이 때 A에 접근 시 A의 주변 메모리에 접근할 확률이 높다.
  • 프로그램 상 Locality
void BubbleSort(int srcArr[], int n)
{
    int i, j, temp; // Temporal Locality
    
    for (i = 0; i < n; ++i)
    {
        for (j = i + 1; j < n - 1; ++j)
        {
            if (srcArr[j] < srcArr[j - 1])
            {
                temp = srcArr[j - 1];
                srcArr[j - 1] = temp; // Spatial Locality
                srcArr[j] = temp;
            } 
        }
   
    }

}

💻 캐쉬 알고리즘

  • 캐쉬 기본 정책
    • 캐쉬 특성 -> Temporal Locality
    • 블록 단위 전송 -> Spatial Locality

캐쉬 힛 = 캐쉬에 요청하는 데이터가 존재
CPU가 캐쉬에 특정 데이터를 요청했을 시 해당 데이터가 캐쉬에 있을 확률이 90% 이상인 건 Spatial Locality의 특성 때문이다.

각 메모리 간 데이터 블럭의 전송 단위가 존재함.
메인 메모리와 하드 디스크 간 전송 단위를 10Mb, 메인 메모리와 L2 캐쉬 간의 전송 단위를 5Mb, L2 캐쉬와 L1 캐쉬 간의 전송 단위를 2Mb, 레지스터는 레지스터의 크기만큼 데이터를 담을 수 있으니까 32비트라고 가정.
레지스터와 L1 캐쉬는 가장 빠르게 데이터를 주고 받고, 메인 메모리와 하드 디스크는 데이터를 가장 느리게 주고 받음.
이러한 속도의 부담을 줄이기 위해 메인 메모리와 하드 디스크의 데이터 전송 단위는 크다.

  • 캐쉬 힛 확률이 높은 이유
    캐쉬에 데이터를 요청했는데 해당 데이터가 없을 경우, 캐쉬는 메인 메모리에 해당 데이터를 요청한다. 이 때 캐쉬는 필요한 데이터 뿐 아니라 주변에 있는 데이터 블럭을 통채로 달라고 요청한다. 프로그램 특성 상 CPU는 잠시 후 주변 데이터에 접근할 확률이 높은데, 이미 주변에 있는 데이터 블럭까지 가져다 놨기 때문에 캐쉬 힛의 확률이 높아진다. 즉 이는 Spatial Locality의 특성 때문.

  • Cache Friendly Code
    arr[i][j]의 크기는 4바이트, 캐쉬와 메인 메모리 간 전송 가능한 데이터 블럭 크기가 16바이트라고 가정할 경우, arr[0][0]을 찾았을 때 해당 데이터부터 총 16바이트의 데이터가 캐쉬에 함께 올라갈 것.
    따라서 한번 arr[i][j]에 접근하면 나머지 3번동안은 캐쉬 미스가 일어나지 않을 것.
... 중략 ...

int nTotal = 0;
for (size_t i = 0; i < SIZEOFARR; ++i)
{
    for (size_t j = 0; j < SIZEOFARR; ++j)
    {
        nTotal += arr[i][j];
    }
}

... 중략 ...

✏️ 가상 메모리(Virtual Memory)

CPU가 하나의 프로그램을 실행하기 위해 필요한 메모리 공간을 2GB라고 가정.
메인 메모리가 가지고 있는 메모리 공간은 256MB 밖에 없음. 이 때 메인 메모리는 프로그램이 요구하는 2GB를 제공하기 위해 하드 디스크를 확장해서 사용함. 하드 디스크는 메인 메모리처럼 데이터를 저장, 삭제하고 비휘발성으로 데이터를 저장할 수도 있음. 이처럼 메인 메모리 공간이 부족할 때 하드 디스크까지 메인 메모리의 영역을 넓히는 것을 가상 메모리 기법이라고 한다.

💻 가상 주소(Virtual Address)

  • 가상 주소가 해결하고 있는 두 가지
    • 선 할당으로 인한 부담
      • 운영 체제에서는 프로세스를 생성할 때마다 32비트 시스템 기준 4GB의 메모리 공간을 할당해준다.
      • 4GB 중 일부는 실행 코드, 커널 코드에 사용. (커널 오브젝트와 관련된 시스템 콜을 하면 유저 모드->커널 모드로 변경되며 커널 코드가 실행이 됨.) 운영체제에 2GB 할당, 사용자에게 2GB 할당.
      • 사용자에게 할당된 2GB 안에는 프로그램의 크기가 포함. 따라서 사용자가 프로그램 실행 중 활용할 수 있는 메모리 용량은 2GB - 프로그램의 크기.
      • 그러나 4GB가 필요하다고 해서 4GB를 선할당하고 아무도 접근할 수 없게 하면 성능 상 손해.
      • 메모리 공간을 필요에 맞게 나눠 써야 함.
    • 느린 속도의 개선
      • 메인 메모리 공간이 부족하면 +@로 하드 디스크를 이용
      • 그러나 프로그램 실행 시 램(메인 메모리)에 접근하는 건 빠르고, 하드 디스크에 접근하는 건 느리다면 말이 안 됨.
      • 가상 메모리 기법을 통해 문제 해결이 가능.

가상 주소와 물리 주소

  • 물리 주소
    램에 할당된 실질적인 주소.
    하드 디스크까지 메인 메모리 영역을 확장하더라도 해당 영역에 존재하는 주소를 물리 주소로 보지 않는다.
    램이 256MB일 경우, 0번지 ~ 256MB - 1번지까지의 주소.
  • 가상 주소
    하드 디스크까지 영역을 확장했을 때 +@로 얻게되는 메모리 주소 값.
    256MB번지부터의 주소.

같은 맥락으로 램 = 물리 메모리, +@ 영역 = 가상 메모리

💻 선 할당으로 인한 부담 해결책

CPU를 손님, MMU(Memory management unit)을 종업원으로 음식점 주인이라고 가정.

프로그래머 뿐만 아니라 CPU 관점에서도 가상 메모리는 존재한다. 따라서 CPU는 자신이 필요한 메모리가 충분히 있다고 생각하고 동작.
따라서 사용자가 2GB-1번지 메모리를 요구할 경우 CPU도 똑같이 2GB-1번지 메모리를 요구. MMU는 그 메모리 공간에 실제로 데이터가 있는 것처럼 2GB-1번지에서 데이터를 가져다 주는 중개 역할을 함.

메인 메모리가 총 16KB이라고 가정할 경우
상황 1) CPU는 더 큰 메모리 공간이 있다고 생각하고 1KB번지부터 20바이트 메모리 할당을 요청.
MMU는 블럭 단위로 램을 할당하기 때문에 들어온 블럭을 0 ~ 4KB번지에 할당.
해당 블럭은 더 이상 다른 용도로 사용 불가능.

상황 2) CPU가 36KB번지부터 20바이트 메모리 할당을 요청. 메인 메모리는 순차적으로 메모리를 사용하는게 아니라, 메모리 블럭의 용도를 순간마다 결정지어버리기 때문에 두번째 블럭을 36KB ~ 40KB로 할당.
해당 블럭은 더 이상 다른 용도로 사용 불가능.

가상 메모리 관점에서 보면 두번째 블럭은 36KB ~ 40KB. 그러나 실제 물리적인 주소 상으로는 4KB ~ 8KB.
이 때 CPU가 36KB번지부터 20바이트 메모리 할당을 요청을 한 뒤 36KB번지에 있는 데이터를 요청한다면, MMU는 물리 메모리 주소 4KB에서 데이터를 가져와야 함.
즉, MMU의 역할을 다음과 같다. CPU가 가상 메모리 주소에 있는 데이터를 요구한다면 MMU는 해당 주소를 물리 메모리 주소로 바꿔서 데이터를 가져온 뒤, 이를 CPU에 건네준다. MMU는 제한된 메모리 공간을 적절히 활용하기 위해 메인 메모리와 CPU 사이에서 컨트롤하는 역할.

소프트웨어적으로 메모리의 블럭 단위 이동 => 페이지(Page)
물리 메모리 관점에서 메모리 블럭 => 페이지 프레임(Page Frame)

페이지 단위로 데이터를 할당 및 해제하기 때문에 페이지와 페이지 프레임의 크기는 일치한다.

💻 느린 속도의 개선

해결법 => 하드 디스크와 램의 관계를 캐쉬 관계로 개선.
프로그램 구현 시 필요한 메모리는 전부 하드 디스크에 있고, Temporal Locality와 Spatial Locality에 의해 필요한 메모리를 블럭 단위로 램에 가져다두고 실행하는 구조. 이는 가상 메모리의 개념.

램이 꽉 찬 상태에서 CPU가 4KB ~ 8KB 메모리 할당을 요구하는 상황.
사용한지 오래 된 메모리 or 사용 빈도가 낮은 메모리 블럭을 하드 디스크에 STORE 한 뒤, 해당 공간에 새롭게 들어온 4KB ~ 8KB 메모리를 할당.
(사용한지 오래 된 메모리의 경우 지역성의 특성에 의해 다시 사용될 확률이 낮음)

이 때 CPU가 다시 8KB ~ 12KB에 할당한 메모리를 쓰겠다고 요청하는 상황.
먼저 사용한지 오래 된 메모리 or 사용 빈도가 낮은 메모리 블럭을 찾아서 하드 디스크로 STORE 한 뒤 해당 자리에 8KB ~ 12KB를 LOAD.
실행 중인 프로그램에 할당된 메모리 공간은 2GB로 크기가 크다. 이 경우 해당 프로그램은 하드 디스크에 파일로 저장됨. (하드 디스크는 파일 시스템 기반으로 데이터를 저장.)
스왑 파일: 프로세스의 가상 메모리 공간 확장을 위해 생성한 파일.
요구하는 데이터를 스왑 파일에서 찾아서 램에 올려두고, 불필요할 시 스왑 파일에 다시 저장.
이처럼 하드 디스크는 저장의 기능만 하는게 아니기 때문에 램만큼 중요도가 높고, 하드 디스크의 용량 또한 중요하다.

💻 둘 이상의 프로세스와 가상 메모리

  • 스왑 파일이 여러 개가 있어도 문제가 되지 않는다.
  • 프로세스 별로 4GB 메모리가 필요한 프로세스가 3개 있으면 총 12GB의 공간 할당이 필요. 이는 각 프로세스의 스왑 파일을 하드 디스크에 저장하기 때문에 가능하다.
    이 때 램은 실행 중인 프로세스와 일체가 되어 메모리 공간을 구성한다.
  • 사실 램을 비우고 채우는 것은 시스템 상 부담스러운 작업이기 때문에 해당 작업은 컨텍스트 스위칭의 범주 안에 포함시켜야 한다.

0개의 댓글