시스템 프로그래밍 노트 6 - 배열과 구조체, 그리고 실수
복잡한 구조의 데이터 관리, 접근을 낮은 레벨에서는 어떻게 하는가? 실수는 어떤 레지스터를 사용하는가?
1. 배열
변수 여러 개를 한꺼번에 관리할 때 사용하는 구조이다.
1.1. 1차원 배열
c에서는 1차원 배열을 다음과 같이 만들 수 있다:
T A[L];
- T는 타입, L은 배열의 길이이다.
- A는 0번째 element의 포인터로 사용된다. 타입은 따라서
T*
.
1.1.1. 배열의 접근
배열의 i번째 원소에 어떻게 접근하는지 예시를 통해서 알아보자.
#define ZLEN 5
typedef int zip_dig[ZLEN];
int get_digit(zip_dig z, int digit){
return z[digit];
}
; %rdi = z, %rsi = digit
get_digit:
movq (%rdi,%rsi,4), %rax
int가 4바이트이기 때문에 z[digit]을 담고 있는 실제 주소는 z + 4*digit이다.
1.1.2. For 문과 섞인 예시
void zincr (zip_dig z){
size_t i;
for (i=0; i < ZLEN; i++)
z[i]++;
}
zincr:
movq $0, %rax
.L4
addq $1, (%rdi,%rax,4)
addq $1, %rax
.L3:
cmpq 4, %rax
jbe .L4
ret
1.2. 다차원 배열
T A[R][C];
1.2.1. 배열의 접근
#define ZLEN 5
typedef int zip_dig[ZLEN];
#define ZLEN2 4
typedef zip_dig zip_dig_pgh[ZLEN2];
zip_dig_pgh pgh = {...};
int get_pgh_digit (int index, int dig){
return pgh[index][dig];
}
; %rdi = index, %rsi = dig
get_pgh_digit:
leaq (%rdi,%rdi,$4), %rax
addl $rax, %rsi ; rsi = 5*index + dig
movl pgh(,%rsi,$4), %rax
ret
zip_dig의 크기가 4*5 = 20바이트이기 때문에 pgh[index][dig]
에 접근하려면 pgh+20index + 4dig
의 값을 불러와야 한다.
1.3. 배열 포인터 배열
zip_dig cmu = {0,0,0,0,0};
zip_dig mit = {0,0,0,0,0};
zip_dif ucb = {1,1,1,1,1};
# define UCOUNT 3
int *univ[UCOUNT] = {mit,cmu,ucb};
int get_univ_digit(size_t index, size_t digit){
return univ[index][digit];
}
; %rdi = index, %rsi = digit
; objective: data in [univ + 8*index] + 4*digit
get_univ_digit:
salq $2, %rsi
addq univ(,%rdi,8), %rsi
movq (%rsi), %rax
ret
univ[index]
는 univ + 8index에 저장된 int *
이고, univ[index][digit]
에 접근하려면 univ+8index에 저장된 값에 4*digit
을 더해서 dereferencing해야 한다.
1.4. n X n 배열
int var_ele(size_t n, int a[n][n], size_t i, size_t j){
return a[i][j];
}
; %rdi = n, %rsi = a, %rdx = i, %rcx = j
; [a+(4*n)*i + 4*j] = [a + 4(ni+j)]
var_ele:
imulq %rdx, %rdi
addq %rdi, %rcx
movq %rsi(,%rcx,$4), %rax
ret
2. 구조체 Struct
struct rec{
int a[4]; // 16바이트
size_t i; // 8바이트
struct rec *next; // 8바이트
}
// a - size_t - next 순서로 저장된다!
void set_val(struct rec *r, int val){
while (r){
int i = r -> i;
r -> a[i] = val;
r = r -> next;
}
}
; %rdi = r, %rsi = val
set_val:
.L11:
movslq 16(%rdi), %rax ; sign extension (%rdi+16), which is long.
movl %esi, (%rdi,%rax,4)
movq 24(%rdi), %rdi
testq %rdi, %rdi
jne .L11
ret
2.1. Alignment
- struct 내부의 각 데이터에 대해 그 데이터의 대표 타입이 K 바이트면, 시작 주소도 K의 배수여야 한다.
- 전체 struct의 위치도 K의 배수여야 한다. 그 K는 모든 element의 가장 큰 alignment이다.
이런 특성으로 큰 데이터 타입을 앞에 두면 공간을 아낄 수 있다. 예를 들면
struct S4 { // 총 12바이트
char c; // 1바이트
int i; // 4바이트
char d; // 1바이트
}
struct S5 { // 총 8바이트
int i; // 4바이트
char c; // 1바이트
char d; // 1바이트
}
2.2. 구조체의 배열
그냥 응용이다.
struct S3 { // 12바이트
short i; // 2바이트
float v; // 4바이트
short j; // 2바이트
} a[10];
short get_j (int idx){
return a[idx].j;
}
get_j:
leaq (%rdi,%rdi,2), %rax
movzwl a+8(,%rax,4), %eax
3. 실수
- XMM 레지스터가 사용된다. 각 16바이트씩 16개가 존재한다.
- %xmm0, %xmm1, …
- 한 레지스터 안에 여러 개의 수를 저장할 수 있다.
- 8 16-bit integers
- 4 single precision floats
- 2 double precision floats
- 1 single precision float
3.1. add 연산
- addss %xmm0, %xmm1: 레지스터 앞쪽의 single precision float addition을 한 번 한다.
- addps %xmm0, %xmm1: 모든 레지스터 영역에 걸쳐 single precision float addition을 한다.
- addsd %xmm0, %xmm1: 레지스터 앞쪽의 double precision float addition을 한 번 한다.
adds로 시작하는 연산을 Scalar 연산, addp로 시작하는 연산을 SIMD 연산이라고 한다.
3.2. 기초
- 인자는 %xmm0, %xmm1, … 을 통해 전달된다.
- 리턴값은 %xmm0이다.
- 모든 XMM 레지스터는 caller-saved 이다.
float fadd(float x, float y){
return x+y;
}
; %xmm0 = x, %xmm1 = y
fadd:
addss %xmm1, %xmm0
ret
3.3. Memory Referencing
mov
instruction은 정수 전용이다. 따라서 실수에 대해서는 다른 instruction이 적용된다.
double dincr(double *p, double v){
double x = *p;
*p = x+v;
return x;
}
; %rdi = p, %xmm0 = v
dincr:
movapd %xmm0, %xmm1; ; double 실수를 다른 레지스터에 그대로 복사할 때 사용되는 명령
movsd (%rdi), %xmm0;
addsd %xmm0, %xmm1;
movsd %xmm1, (%rdi)
ret
movapd는 aligned packed-doubles의 약자이다.
Leave a comment