[컴퓨터 시스템] 배열의 할당과 접근
컴퓨터 시스템 책 3장 프로그램의 기계수준 표현에서 3.8 배열의 할당과 접근
에 대해 정리한 글입니다.
3.8 배열의 할당과 접근
C 언어에서는 배열 원소들에 대한 포인터를 만들 수 있으며, 이 포인터들 간에 연산도 가능합니다
이러한 연산은 결국 기계어 수준에서는 주소 계산으로 변환됩니다
최적화 컴파일러는 배열 인덱스를 사용할 때 필요한 주소계산을 단순화하는 데 특히 우수한 성능을 보입니다
그 이유는 반복되는 주소 계산을 미리 해둬서 더 빠르게 돌게 하기 때문입니다
예를 들어 다음과 같은 배열 선언이 있다고 가정해보자.
int arr[5] = {10, 20, 30, 40, 50};
배열은 메모리에 아래와 같이 연속적으로 저장이 된다
int
형은 4byte이므로, 각 요소는 4byte 간격으로 저장이 된다
주소 | 0x100 | 0x104 | 0x108 | 0x10C | 0x110 |
---|---|---|---|---|---|
값 | 10 | 20 | 30 | 40 | 50 |
C에서 배열에 접근하는 것은 사실상 주소를 계산하는 것과 같다
예를 들어 int x = arr[2];
는 아래와 같이 포인터 연산으로도 표현할 수 있다
x = *(arr+2);
배열의 시작 주소(arr)에서 두번째 요소만큼 이동한 위치의 값을 가져오는 것이다
이러한 연산은 컴파일 시 어셈블리 코드로 변환된다
movl 8(%rdi), %eax
8(%rdi)
:rdi
레지스터가 가리키는 주소에서 8byte 떨어진 위치의 값을 의미%eax
: 목적지 레지스터, 여기에 값을 저장함
즉 8(%rdi)
는 8byte 떨어진 3번째 요소를 뜻한다
결과적으로 x
에는 30
이 저장된다
3.8.1 기본 원리
배열 선언의 형태와 의미
1
2
3
4
char A[12];
char *B[8];
int C[6];
double *D[5];
Array | Element size | Total size |
---|---|---|
A | 1 | 12 |
B | 8 | 64 |
C | 4 | 24 |
D | 8 | 40 |
배열 B, D는 모두 포인터의 배열로, 배열의 원소는 각각 8byte 크기를 갖는다
배열 접근
Q. x86-64
에서 배열 접근은 어떻게 처리가 될까요?
x86-64
의 메모리 참조 인스트럭션은 배열 접근을 간단하고 빠르게 수행할 수 있도록 설계되었다
1
int E[10];
예를들어, E[i]에 접근하려면 실제로 배열의 시작 주소에 i * 4
byte를 더한 위치를 참조해야한다
int
타입이 4byte이기 때문이다
[어셈블리 코드에서 E[i]
에 접근하는 과정]
E
배열의 시작 주소가%rdx
에 저장되어있고i
가%rcx
에 저장되어있다면,
아래와 같은 어셈블리 인스트럭션을 사용할 수 있다
movl ($rdx, $rcx, 4), %eax
이 인스트럭션은 주소 계산을 수행하게 된다
주소 = E + i * 4
해당 주소에 저장된 값을 %eax
레지스터에 저장한다
3.8.2 포인터 연산
C는 포인터 간에 연산을 허용하며, 계산된 값은 포인터가 참조하게 되는 자료형의 크기에 따라 그 값이 확대된다.
단항연산자 &
와 *
는 포인터의 생성과 역참조를 수행한다
1
2
int x= 10;
int *p = &x;
p
는 x
의 주소를 저장하는 포인터다
*p
는 p
가 가리키는 값 10
을 준다
포인터에 숫자 더하기
그냥 숫자처럼 더하는 게 아니라, 자료형의 크기를 기준으로 이동한다
1
2
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
p
:arr[0]
의 주소p
+ 1 :arr[1]
의 주소p
+ 2 :arr[2]
의 주소*(p+2)
:arr[2]
의 값 → 30
포인터 뺄셈
두 원소 사이의 거리를 구하는 것이다
아래의 경우는 4칸 차이가 나므로, 4이 출력된다
1
2
3
4
int arr[5] = {10, 20, 30, 40, 50};
int *a = &arr[4];
int *b = &arr[0];
int minus = a - b; // 4
3.8.3 다중 배열
C에서 2차원 배열은 1차원 배열의 배열이다
배열의 원소들은 메모리에 행 우선(row major)으로 쌓인다
따라서 배열에 순서대로 접근 할 때, 행 우선 순서로 접근하는 것이 좋다
arr[i][j]
의 주소는 &arr[i][j] = arr + (i * 열의 개수 + j)
로 계산된다
예를 들어보자
1
int A[5][3];
위와 같이 배열을 선언하면, A는 5개의 행과 3개의 열을 가지는 2차원 배열이다
각 원소는 int
타입이고, 각 int
는 4byte 이니까 총 크기는 4byte * 5행 * 3열 = 60byte
이다
또한 &A[2][1]
의 주소를 계산해보면 A + (2 * 3 + 1) = A + 7
로 계산된다
즉 A[2][1]
은 배열의 7번째 요소에 해당한다
typedef
를 이용한 표현 방식
1
2
typedef int row3_t[3];
row3_t A[5];
row3_t는 3개의 정수로 이루어진 배열
A는 이런 배열을 5개 가지는 배열이다
- 메모리엔 행 우선 저장 방식을 사용해 저장된다
1
2
3
4
5
A[0][0], A[0][1], A[0][2],
A[1][0], A[1][1], A[1][2],
A[2][0], A[2][1], A[2][2],
A[3][0], A[3][1], A[3][2],
A[4][0], A[4][1], A[4][2]
한 행의 모든 열이 먼저 저장되고, 그 다음 행으로 넘어가는 방식이다