본문 바로가기

C, C++/뇌를 자극하는 윈도우즈 시스템 프로그래밍

[뇌를 자극하는 윈도우즈 시스템 프로그래밍] 16장 컴구조(4)

메모리 계층

https://ko.wikipedia.org/wiki/%EB%A9%94%EB%AA%A8%EB%A6 %AC_%EA%B3%84%EC%B8%B5_%EA%B5%AC%EC%A1%B0

컴퓨터의 메모리는 단일한 공간이 아니라, 레지스터, 캐시, 메인 메모리(RAM), 그리고 SSD/HDD 같은 저장 장치로 구성된 계층적 구조를 가진다. 이들 메모리는 상위일수록 속도는 빠르지만 용량이 작고 비용이 크며, 하위일수록 속도는 느리지만 용량이 크고 저렴하다.

프로그램 실행 시에는 하위 계층의 데이터를 상위 계층으로 점진적으로 불러와 사용한다. 예를 들어, 프로그램은 디스크에서 RAM으로 로드되고, 이후 CPU는 실행 중 필요한 데이터를 캐시와 레지스터로 옮기며 명령어를 수행한다.

따라서 단순히 "메모리에 저장된다"는 표현은 이처럼 다양한 수준의 저장소와 계층적 흐름을 고려하지 않기 때문에 모호하고 부정확할 수 있다.

 


캐시와 캐시 알고리즘

https://basaeng.tistory.com/32

 

컴퓨터의 CPU (3) - 캐시 메모리

https://basaeng.tistory.com/27 컴퓨터의 CPU (2) - 레지스터https://basaeng.tistory.com/7 컴퓨터의 CPU (1)메인 메모리를 알아봤으니 다음은 CPU이다.구매한 CPU는 가장 보급, 엔트리 급으로 많이 팔리는 라이젠 7500f

basaeng.tistory.com

캐시의 공간 지역성과 시간 지역성

캐시의 크기는 작기 때문에 접근 순서에 따라 캐시히트 여부가 달라질 수 있다.

const int Size = 2048;
int arr[Size][Size];  // 약 16MB

void row_major_access() {
    long long sum = 0;
    auto start = chrono::high_resolution_clock::now();

    for (int i = 0; i < Size; ++i)
        for (int j = 0; j < Size; ++j)
            sum += arr[i][j];

    auto end = chrono::high_resolution_clock::now();
    cout << "Row-major time: "
        << chrono::duration_cast<chrono::milliseconds>(end - start).count()
        << " ms\n";
}

void col_major_access() {
    long long sum = 0;
    auto start = chrono::high_resolution_clock::now();

    for (int j = 0; j < Size; ++j)
        for (int i = 0; i < Size; ++i)
            sum += arr[i][j];

    auto end = chrono::high_resolution_clock::now();
    cout << "Col-major time: "
        << chrono::duration_cast<chrono::milliseconds>(end - start).count()
        << " ms\n";
}

int main() {
    // 초기화
    for (int i = 0; i < Size; ++i)
        for (int j = 0; j < Size; ++j)
            arr[i][j] = 1;

    row_major_access(); 
    col_major_access();  

    return 0;
}

다음과 같은 코드의 경우 16MB를 순차적으로 접근한다.

이 때 L1은 32KB, L2는 6MB 이기 때문에 (나의 PC기준)

두 개의 함수 실행시간이 다르다.

결과를 보면 두 배 넘는 차이를 보인다.

 

또한 캐시 교체 알고리즘 등의 영향으로 시간 지역성의 성질도 보인다.


가상 메모리(Virtual Memory)

현재 시스템의 물리 메모리(RAM)가 32GB정도를 가지고 있다면 32GB보다 큰 프로그램은 실행할 수 없는 것일까?

그렇지는 않다. 예를 들어, 발더스 게이트 3 같은 게임은 설치 용량만 100GB를 넘지만, 정상적으로 실행되고, 그와 동시에 여러 백그라운드 프로그램도 함께 실행된다.

 

이러한 것이 가능한 이유는 바로 가상 메모리(Virtual Memory) 덕분이다.

가상 메모리를 사용하면 운영체제는 각 프로세스에 독립된 가상 주소 공간을 제공할 수 있다.

기본적으로 32bit 시스템이라면 4GB를, 64bit라면 128TB의 논리적으로 독립된 공간을 제공한다.


그리고 가상 메모리는 필요한 데이터만을 메모리에 적재하고, 나머지는 디스크(페이지 파일 등)에 보관해 두었다가
필요할 때 불러오는 방식(지연 적재, demand paging)을 사용한다.

 

이로 인해 실제 RAM보다 훨씬 큰 프로그램도 실행이 가능하고, 동시에 여러 프로세스가 실행될 수 있으며, 프로세스 간 메모리 보호와 격리도 자연스럽게 이루어진다.

 

페이징

가상 메모리를 이해하기 위해서는 페이징이라는 개념의 이해가 필요하다.

가상메모리에 존재하는 어느 데이터가 물리 메모리의 어느 공간에 있는지 알기 위해서는 매핑이 필요하다.

이 때 가상 메모리와 물리 메모리는 페이지 단위로 매핑된다.

https://ko.wikipedia.org/wiki/%ED%8E%98%EC%9D%B4%EC%A7%95

 

페이징 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 페이징 기법(paging)은 컴퓨터가 메인 메모리에서 사용하기 위해 2차 기억 장치[a]로부터 데이터를 저장하고 검색하는 메모리 관리 기법이다.[1] 즉 가상기억장치

ko.wikipedia.org

 

가상 메모리 페이지의 크기는 기본적으로 4KB이며, 이에 대응되는 물리 메모리 프레임의 크기또한 4KB이다.

 

매핑은 MMU(Memory Management Unit)의 역할이다.

MMU는 PTBR을 참조해 각 프로세스마다 다른 PTE의 위치를 찾고 이를 통해 매핑 정보를 찾을 수 있다.

 

가상, 물리 메모리간 매핑을 위해 주소를 태그, 인덱스, 오프셋과 같이 나눈다.

오프셋은 6비트로 표현한다. 캐시 라인 내의 위치와 같다고 보면 된다.

인덱스는 페이지 테이블 내에서 인덱스 번호를 뜻한다. (6비트)

태그는 페이지 테이블에서 실질적으로 비교하는 값이다. (나머지)

 

인덱스 + 오프셋의 비트는 12비트인데 이는 4KB인 페이지의 크기와 같다.

이를 활용해 더 쉽게 매핑할 수 있는데 이를 VIPT구조라고 한다.

 

Swap out, in

프로그램의 가상 메모리에서는 사용하는 모든 메모리를 매핑하지는 않지만 여러 프로그램을 실행하면 RAM의 크기가 부족할 수 있다. 

이 때 OS는 사용하지 않는 페이지를 잠시 디스크로 swap out한다. 그리고 다시 접근이 있는 경우 swap in하여 사용한다.

디스크로 프레임을 옮기는 것이기 때문에 속도저하가 있을 수 있다.


매우 간단하게 정리했으니 자세한 것은 찾아봐야 한다.