C언어 에서의 배열 선언 T A[N]
(T
는 자료형, N
은 정수 길이)은 두 가지 효과를 갖는다.
1. 바이트(은 자료형T
의 크기) 만큼의 공간을 하나의 블록(연속적인 메모리 공간)으로 메모리에 할당한다.
2. 식별자 A
를 배열 시작 주소() 포인터로 사용한다.
따라서 배열의 번 원소(A[i]
)는 주소 에 저장된다.
포인터 변수도 결국에는 주소 값을 가지는 변수이기 때문에 연산을 사용할 수 있다.
정수 연산과 다른 점이 있다면 만큼을 1로 하여 연산한다.
즉 &A[3] - &A[2]
은 이 아니라 1이다.
T A[N][M]
은 바이트 만큼의 공간을 할당한다.
A[i][j]
는 에 접근한다.
A[i]
는 주소를 가리킨다.
C 컴파일러는 고정크기의 다차원 배열을 사용하는 코드에 대해 다양한 최적화를 수행할 수 있다.
// 고정 크기 배열
#define N 16
typedef int fix_matrix[N][N];
fix_matrix[i][j]
의 주소는 로 구할 수 있다.
하지만 연속적인 인덱스에 접근해야 할 때 매 번 곱셈과 덧셈을 반복하는 것은 비효율적이다.
컴파일러는 이런 경우에 이전 인덱스를 기억해두었다가 씩 더하는 방식으로 순회를 진행하도록 최적화한다.
특히 고정크기의 배열인 경우에는 그 크기 또한 컴파일 단계에서 확정할 수 있으므로 인덱스를 저장할 변수도 사용하지 않도록 최적화할 수도 있다.
C는 이제 아래와 같은 가변크기 배열 할당을 허용한다.
int A[i][j];
int *A = (int *)malloc(sizeof(int)*i*j)
프로세스는 직접 물리 메모리 주소에 접근하는 것이 아니라 프로세스 각각이 갖는 가상 주소 공간을 통해서 메모리에 접근하게 된다.
단순히 부족한 메모리 공간을 확장하기 위한 수단을 넘어 효율적인 메모리 관리, 멀티태스킹, 메모리 보호, 프로세스 격리 등 다양한 기능을 한다.
따라서 가상메모리를 이해하는 것은 시스템이 어떻게 동작하는지 이해하고, 프로그램의 성능을 향상시키고 에러를 피하는데 도움을 준다.
초기의 PC, 디지털 신호 처리 프로세서, 임베디드 컨트롤러 등은 물리 주소 방식을 사용한다.
하지만 대부분의 현대 프로세스들은 가상주소방식을 사용한다.
CPU는 가상주소지정으로 가상주소(VA)를 생성해서 메인 메모리에 접근한다.
물리 메모리에 접근하기 전에 메모리로 보내지기 전에 메모리 관리 유닛(MMU)가 적절한 물리주소로 번역한다.
가상메모리는 디스크에 저장되며 1바이트 크기 배열로 구성된다.
디스크 안의 배열 정보는 메인 메모리로 캐시된다.
캐시는 블록 단위로 분할되어 디스크과 메인 메모리 사이를 잇는 역할을 하며 가상페이지라고 불린다.
물리메모리 또한 물리페이지(물리 프레임)로 분할되어 사용된다.
가상페이지는 언제나 셋 중 하나로 분류할 수 있다.
DRAM 캐시: 메인 메모리로 캐시하는 가상페이지 캐시
SRAM 캐시: L1, L2, L3 캐시 메모리로 캐시하는 메인 메모리 캐시
DRAM과 SRAM의 차이보다 디스크과 DRAM의 차이가 훨씬 더 크다.
따라서 DRAM 캐시의 미스를 줄이는 것은 중요하다.
VM 시스템은 가상페이지가 DRAM 어디에 있는지, 어떤 물리 페이지를 캐싱했는지, 없다면 디스크 어디에 가상 페이지가 있고 어떤 페이지를 물리 메모리에서 제거할 것인지를 결정해야 한다.
이런 VM 시스템의 기능들은 운영체제, MMU의 주소 번역 하드웨어, 페이지테이블(가상페이지를 물리페이지로 매핑하는 자료구조. 물리메모리에 저장)의 조합으로 제공된다.
VM은 메모리 관리를 단순하게 하는 목적으로도 사용된다.
mmap
)사용자 프로세스는 읽기 전용 코드를 수정할 수 없어야 하며,
커널 코드나 데이터 역시 읽거나 수정할 수 없어야 한다.
또한 다른 프로세스의 사적 메모리에 접근할 수 없어야 한다.
VM을 통해 별도의 가상 주소공간을 제공하면 이런 분리가 쉬워진다.
PTE에 허가 비트를 추가해서 접근을 제어하는 방식이 가능해진다.
동적 메모리 할당기는 가상메모리의 힙(heap) 영역을 관리한다.
할당기는 힙을 다양한 크기의 블록들로 나누어 관리한다.
각 블록은 할당되었거나 가용한(free) 가상메모리의 연속적 묶음이다.
할당기는 크게 두 종류가 있다.
free
함수를 사용한다.malloc
과 free
함수C 프로그램은 malloc
함수를 호출해서 힙으로부터 블록들을 할당받는다.
malloc
함수는 요청된 size 이상의 메모리 블록의 포인터를 리턴한다.
32비트 모드(gcc -m32
)에서 항상 8의 배수인 주소를 리턴하며, 64비트 모드에서는 16의 배수인 주소를 리턴한다.(정렬 제한사항)
32비트 모드 기준으로 주소를 2진법으로 표현했을 때 마지막 세 자리가 000이라는 의미이다.
free
함수는 블록의 시작 주소를 인자로 받아 블록을 반환하며, 인자가 블록의 시작 주소가 아닌 경우 아무런 동작도 하지 않는다.
가장 중요한 이유는 프로그램을 실제 실행시키기 전에는 자료 구조의 크기를 알 수 없는 경우들이 있기 때문이다.
명시적 할당기에 요구되는 사항:
성능 좋은 할당기는 처리량과 메모리 이용도를 최대화한다.
처리량과 이용도는 서로 상충관계에 있으므로 할당기 설계에서 적절한 균형을 찾는 것이 중요하다.
[^1]: 단조 증가 가정. 힙의 최대 크기로 정의하면 단조 증가 가정을 완화할 수 있다.
가용 메모리가 할당 요청을 수행할 수 없는 상황을 단편화라고 한다.
단편화는 두 가지 종류로 분류된다.
malloc
과 free
함수|정렬 제한사항]]을 만족시키기 위해 데이터보다 블록 크기를 증가시킬 수 있기 때문이다.brk
확장)해야 한다.외부 단편화는 예측하기 어렵기 때문에 할당기들은 보통 가용 블록들의 크기를 크게 유지하려는 방법을 사용한다.
[^2]: 가상 메모리에서 워드는 4바이트, 더블 워드는 8바이트
항상 요청한 크기만큼 힙을 증가시키고 그 이전 힙 경계 주소를 반환하는 간단한 할당기를 생각해볼 수 있다.
이 할당기는 처리량은 매우 좋지만 이용도는 매우 나쁠 것이다.
처리량과 이용도 사이에 좋은 균형을 유지하는 할당기는 다음 이슈들을 고려해야 한다:
아래에서는 위의 이슈들의 간단한 구현인 묵시적 가용 리스트를 중심으로 설명한다.
가용 블록 구성을 구현하기 위해
이를 가용 리스트라고 한다. 대부분의 할당기는 이 정보를 블록 내에 저장한다.
그 중에서도 간단한 구현인 묵시적 가용 리스트는 메모리 블록을 데이터, 패딩에 덧붙여 1워드 길이의 헤더를 포함한다.
헤더는 블록의 크기(헤더와 패딩을 포함한 전체 블록)와 가용 여부에 대한 정보 등을 담게된다.
헤더의 한 가지 간단한 예로 위와 같은 구조를 생각할 수 있다.
32비트 모드를 가정할 때 블록의 크기는 최대 1워드이다.
그러나 정렬 제한사항을 고려하면 할당기는 블록의 크기를 8의 배수로 유지[^3]하므로 블록 크기(2진법)의 하위 3비트는 항상 0이다.
그러므로 하위 3비트는 다른 정보를 위해 사용한다.
이 예시에서는 하위 1비트에 할당/가용 여부를 표시하는 비트로 사용하였다.
[^3]: 데이터를 보관하는 payload의 시작 주소는 8의 배수여야 한다. 헤더의 크기가 4바이트임을 감안하면, 헤더를 포함한 블록의 시작주소(=이전 블록의 끝 주소)는 8n-4여야 한다. 따라서 할당기는 블록의 크기가 8의 배수가 되도록 패딩한다.
가용 리스트도 이름처럼 추상적 자료형인 리스트의 일종이다.
묵시적 가용 리스트는 리스트를 구현하는데 있어 배열이나 연결리스트와 다르게 블록 자신의 크기와 가용 부를 표시함으로써 다음 가용 블록의 위치를 묵시적으로 알린다.
최소 블록 크기는 시스템의 정렬 요구사항과 할당기의 블록 포맷 선택에 의존한다.
예시: 정렬 요구사항을 더블 워드(8바이트), 블록 포맷을 묵시적 가용 리스트라고 하면 최소 블록 크기는 데이터 4바이트, 헤더 1바이트, 패딩 3바이트로 8바이트가 된다.
할당기가 요청받은 블록을 저장하기 위해 가용 블록을 검색하는 방법을 배치 정책이라고 한다.
아래에서는 할당 블록까지 검색하지 않을 수 있도록 단순화한 best-fit 정책인 분리 가용 리스트 조직 segregated free list organizations에 대해서 설명한다.
할당기가 블록을 배치할 가용 블록을 찾은 후에는 가용 블록의 어느 정도를 할당할 것인지 결정해야 한다.
배치할 블록을 찾을 수 없다면, 우선 인접한 가용블록들을 합쳐서(연결) 더 큰 가용 블록들을 만들어 본다.
이미 모두 연결돼있거나 충분히 큰 블록이 만들어지지 않으면 sork
함수를 호출해서 추가적인 힙 메모리를 요청한다.
블록을 반환했을 때, 인접한 가용 블록이 있다면 하나의 연결된 가용블록으로 볼 수 있다.
하지만 실제로는 가용블록이 나누어져 있어 배치에 실패하는 경우가 생길 수 있고 이를 가성 단편화(false fragmetation)라고 한다.
가성 단편화를 극복하기 위해 할당기는 가용블록들을 합칠 수 있다(연결 coalescing).
즉시 연결은 간단하며 상수 시간()에 수행할 수 있는 장점이 있지만
할당-반환을 반복하는 프로그램에서는 연결과 분할을 반복하는 등 일부 요청 패턴에서 쓰래싱이 발생할 수 있다.
빠른 할당기들은 지연 연결을 주로 사용하지만 아래에서는 설명을 위해 즉시 연결을 가정한다.
반환하는 블록(현재 블록)의 다음 블록이 가용 블록인지 확인하려면 헤더에서 블록 크기만큼 이동하면 된다.
하지만 이전 블록이 가용 블록인지 확인하기 위해서는 헤더만 있는 묵시적 가용 리스트라면 처음부터 블록들을 검색해보는 방법밖에 없다.[^4]
이를 위해 경계 태그라는 기법이 등장하였다.
경계 태그는 헤더와 동일한 내용을 블록 끝에 풋터로 추가하는 것으로, 블록 앞 1워드만을 읽어 이전 블록이 가용 블록인지 확인할 수 있다.
다만 각 블록마다 헤더와 풋터를 유지해야 하므로 메모리 오버헤드가 심해진다.
풋터는 이전 블록이 가용 블록일 경우 크기를 알려주는 목적으로 사용되므로, 이전 블록이 가용 블록인지 여부를 헤더에 저장하고, 가용 블록이 아닌 경우(할당 블록인 경우)에는 풋터를 사용하지 않는 방식으로 최적화할 수 있다.
[^4]: 이전으로 워드 크기씩 헤더를 찾아가는 방법을 떠올릴 수 있지만, 헤더를 통해 블록 크기만큼 이동하는 컨텍스트가 없다면 헤더를 찾더라도 헤더인지 데이터인지를 판단할 수 없다.
지금까지 묵시적 가용 리스트를 통해 할당기의 개념을 소개하였다.
그러나 묵시적 가용 리스트는 블록 할당 시간이 전체 힙 블록의 수에 비례하기 때문에[^5] 범용 할당기에 적합하지 않다.
보통 명시적 자료구조로 가용 리스트를 만드는 것이 좋은 방법이다.
가용 블록은 프로그램에서 사용하지 않기 때문에 가용 블록의 리스트를 구현하기 위해 가용 블록 내의 공간을 사용할 수 있다.
그림은 명시적 가용 리스트의 간단한 예시로, 가용 블록 안에 이전/다음 가용 블록의 주소를 저장해 이중 연결 리스트로 구성했다.
명시적 가용 리스트를 사용하면
다만 first fit 배치 정책을 사용할 때의 반환 시간과 이용도는 블록 정렬 정책에 의해 달라진다.
[^5]: 가용 블록의 수를 N, 할당 블록의 수를 M이라고 한다면 묵시적 가용 리스트의 블록 할당 시간은 이다.
[^7]: [[#9-9-7. 할당한 블록의 배치]]에서 알 수 있듯 first fit의 장점은 뒷 부분에 크기가 큰 가용 블록이 배치된다는 점에서 발생한다. 후입 선출로 정렬하였을 때에는 크기와 관계 없이 반환된 순으로 정렬되므로 이런 장점이 사라진다.
앞서 알아본 모든 가용 블록들을 하나의 연결 리스트로 사용하는 방법을 단일 연결 가용 블록 리스트라고 한다.
이는 할당 시간이 비례^6하므로 할당 시간을 줄이기 위해 분리 저장장치 segregated storage를 사용하는 경우가 많다.
분리 저장장치 방식은 가용 리스트를 비슷한 크기(크기 클래스size class) 별로 여러 개로 나누어 사용한다.
크기 클래스를 나누는 방식 예시로는 2의 제곱 단위로 나누는 방법
또는 더 세분하여 크기가 작은 블록들은 각자의 크기 클래스에, 큰 블록들은 2의 제곱으로 나누는 방법
등이 있다.
각 크기 클래스마다 크기 순으로 정렬된 가용 리스트를 가지게 된다.
분리 저장장치의 예시로 간단한 분리 저장장치를 들 수 있다.
각 크기 클래스의 가용 블록 크기는 모두 클래스의 최대 크기이다.
예를 들어 2의 제곱으로 클래스를 구분하면 가용 블록은 1, 2, 4, 8, 16...의 크기만을 갖는다.
[^8]: 할당과 반환을 반복하는 프로그램이 요청하는 블록의 크기를 점점 키워가는 경우, 작은 크기 클래스의 블록들은 가용상태로 남아있으나 연결되지 않으므로 사용할 수 없다. 즉, 외부 단편화가 유발된다.
분리 맞춤은 리스트들이 다양한 크기의 블록을 가질 수 있다.
분리 맞춤 방식의 분리 저장장치는 묵시적 가용 리스트에도 적용될 수 있다.
다음은 분리 맞춤의 간단한 예시이다.
버디 시스템은 크기 클래스가 2의 제곱인 경우에 사용할 수 있는 분리 맞춤(segregated fits) 방식이다.