정렬 제한이란 데이터가 메모리에 저장될 때 해당 메모리의 주소에 대한 제약 사항을 말하는 것이다.
구체적으로는 데이터가 저장될 위치가 특정한 단위로 정렬(배치?) 되어 있어야 한다는 것을 뜻한다.
예를 들어 CPU 아키텍처에서 메모리 접근 시에 32비트 단위로 정렬된 주소에만 접근할 수 있다고 하자.
위의 데이터 A는 주소값이 4의 배수 형태이므로 정렬되어 있지만, B의 경우는 그렇지 않다.
B의 경우 해당 데이터에 접근 시 실행 환경에 따라 다음과 같은 여러 가지 결과를 일으킬 수 있다.
- 비정상적인 메모리 접근을 인식하여 OS가 프로그램을 종료시킨다.
- CPU 혹은 컴파일러가 B가 속한 두 메모리 영역에 접근한 뒤 적절한 비트 연산을 거쳐 원하는 값을 만들어준다.
- 주소를 적당히 4의 배수로 변경하여 메모리에 접근한다.
- 별 문제없이 실행된다.
어떤 경우이든 정렬되지 않은 메모리에 대한 접근은 성능에 도움이 되지 않는다.
최악(?)의 경우 B가 걸쳐있는 두 메모리 워드가 각자 다른 캐시 라인에 속할 수도 있음을 생각해 보자.
따라서 컴파일러는 컴파일 시에 이러한 정렬 제한을 고려하여 데이터를 메모리 상에 배치한다.
(물론 최종적으로는 링커가 이러한 작업을 수행한다.)
C 언어의 기본 데이터 타입은 모두 자신의 크기와 동일한 정렬 제한을 가진다.
gcc에서 이러한 정보는 sizeof와 __alignof__를 통해서 얻을 수 있다.
#include <stdio.h>
char c;
short s;
int i;
long l;
float f;
double d;
int main(void)
{
#define print_align(type, var) \
printf(#type "\t%zd\t%zd\t%p\n", sizeof(type), __alignof__(type), &var)
printf("type\tsize\talign\taddress\n");
print_align(char, c);
print_align(short, s);
print_align(int, i);
print_align(long, l);
print_align(float, f);
print_align(double, d);
return 0;
}
위의 예제 프로그램을 빌드한 후 실행해보면 다음과 같은 결과를 얻을 수 있다.
$ ./a.out
type size align address
char 1 1 0x40209a
short 2 2 0x402098
int 4 4 0x402094
long 8 8 0x402088
float 4 4 0x402090
double 8 8 0x402080
데이터 타입의 크기와 정렬 제한은 모두 동일한 크기를 가지며
실제 변수가 생성된 주소도 이러한 정렬 제한에 맞추어 졌음을 볼 수 있다.
좀 더 자세히 살펴보면 동일한 크기/정렬 제한을 가지는 데이터끼리 묶어서 배치하였으며
공간을 효율적으로 사용하기 위해 큰 크기/정렬 제한을 가지는 데이터부터 배치한 것도 볼 수 있다.
정렬 제한은 포인터 변환 시에도 반드시 고려해야 할 부분인데
cast 연산을 통해 강제로 변환한 주소값이 변환된 포인터가 가리키는 데이터 타입의
정렬 제한을 어길 수 있기 때문이다. 예를 들어 위의 예제에서
변수 i의 주소를 long * 타입으로 변환한다면 long 타입이 요구하는 8 바이트의 정렬 제한을
만족시키지 못하므로 해당 포인터를 통해 메모리에 접근 시 위에서 언급한 결과 중의 하나를 얻게 될 것이다.
이제 본격적으로 구조체에 대해서 살펴보기로 하자.
구조체는 여러 데이터 타입을 모아둔 집합체의 개념이므로 구조체를 이루는 각 멤버에 대해서도
위에서 언급한 정렬 제한을 모두 만족시켜야 한다. 또한 구조체는 위의 예제에서와 같이
메모리를 효율적으로 사용하기 위해 구조체 멤버의 순서를 임의로 조정할 수 없으며
반드시 구조체 선언 시에 명시된 순서대로 멤버를 메모리 상에 배치해야 한다.
따라서 이를 조정하기 위해 멤버 사이에 사용하지 않는 패딩 공간을 할당할 수 있다!!
구체적인 예를 통해 살펴보기로 하자.
#include <stdio.h>
struct sc {
char c;
} sc;
struct sl {
long l;
} sl;
struct scl {
char c;
long l;
} scl;
struct slc {
long l;
char c;
} slc;
int main(void)
{
#define print_align(type, var) \
printf(#type "\t%zd\t%zd\t%p\n", sizeof(type), __alignof__(type), &var)
printf("type\t\tsize\talign\taddress\n");
print_align(struct sc, sc);
print_align(struct sl, sl);
print_align(struct scl, scl);
print_align(struct slc, slc);
return 0;
}
실행 결과를 살펴보기 전에 먼저 결과가 어떻게 나오게될 지 생각해 보자.
멤버로 오직 하나의 데이터 만을 가지고 있는 sc와 sl의 경우는
단순히 해당 멤버의 크기와 정렬 제한을 그대로 갖게될 것이다.
하지만 scl과 slc와 같이 서로 다른 데이터 타입으로 이루어진 경우라면 어떨까?
위의 예제를 실행해 보면 다음과 같은 결과를 얻을 수 있다.
$ ./a.out
type size align address
struct sc 1 1 0x4020a8
struct sl 8 8 0x4020a0
struct scl 16 8 0x402080
struct slc 16 8 0x402090
앞서 말한대로 sc와 sl의 경우에는 특별한 사항이 없다.
scl의 경우에는 우선 정렬 제한이 8이라고 나오는데 이는 멤버 중의 가장 큰 정렬 제한을
가지는 long 타입의 정렬 제한을 그대로 물려받은 것이다.
또한 크기는 9가 아닌 16으로 나오는데 이는 8 바이트 단위로 정렬된 위치에 첫 멤버인
char 타입의 데이터가 저장되고 그 이후에 long 타입의 데이터가 다시 8바이트 단위로
정렬된 위치에 저장되기 때문이다. 즉 1~7번 바이트는 사용되지 않고 단순히 long 타입의
멤버의 정렬 제한을 보장하기 위한 용도로 채워진 패딩 바이트인 것이다.
구조체 내의 각 멤버의 위치는 offsetof 매크로를 통해 조사할 수 있는데
위의 경우 offsetof(struct scl, l)의 값은 8이 될 것이다.
slc의 경우에는 마찬가지로 정렬 제한이 8이고 크기가 16으로 나오는데
정렬 제한이야 그렇다 하겠지만 크기는 왜 9가 아닌 16으로 나오는 것일까?
만약 크기가 9이더라도 long 타입과 char 타입의 정렬 제한을 이미 모두 만족시키고 있는데
왜 굳이 구조체 뒤쪽에 불필요한 패딩 바이트를 추가하여 크기를 크게 만들었을까?
그 해답은 바로 배열 때문이다.
알고 있듯이 배열은 구조체와 같은 집합체이지만 동일한 데이터 타입으로 이루어진 것이며
중요한 사항은 배열을 이루는 각 원소들은 메모리 상에서 연속된 영역에 존재해야 하며
각 원소들 자체에 대해서도 정렬 제한을 만족시켜야 한다는 점이다.
만일 slc의 크기가 9가 된다면 배열의 다른 요소들은 정렬 제한을 만족하지 못할 것이므로
slc 내에 패딩을 포함시켜 slc의 크기 자체를 정렬 제한의 배수가 되도록 맞춘 것이다.
이렇게 구조체 내의 패딩 바이트가 추가되는 규칙을 알고 있다면
구조체 선언 시 각 멤버들의 위치를 잘 선택하여 효율적인 메모리 배치를 이루도록 할 수 있을 것이다.
하지만 상황에 따라 이러한 구조체의 크기/정렬 제한을 조정해야 하는 경우가 있을 수 있는데
(일반적으로는 그리 추천할 만 한 방법은 아닐 것이다)
이 때는 gcc에서 확장 기능으로 제공하는 aligned 혹은 packed 속성을 이용하면 된다.
역시 예제를 통해 살펴보기로 하자.
#include <stdio.h>
struct asc {
char c __attribute__((aligned(8)));
} sc;
struct __attribute__((aligned)) asl {
long l;
} sl;
struct __attribute__((packed)) pscl {
char c;
long l;
} scl;
#pragma pack(push, 2)
struct pslc {
long l;
char c;
} slc;
#pragma pack(pop)
int main(void)
{
#define print_align(type, var) \
printf(#type "\t%zd\t%zd\t%p\n", sizeof(type), __alignof__(type), &var)
printf("type\t\tsize\talign\taddress\n");
print_align(struct asc, sc);
print_align(struct asl, sl);
print_align(struct pscl, scl);
print_align(struct pslc, slc);
return 0;
}
앞서 말한대로 구조체의 정렬 제한은 멤버 중 가장 큰 값을 물려받는다.
따라서 구조체의 정렬 제한을 변경하는 대신 멤버의 정렬 제한을 변경한 경우에도
구조체에 영향을 미치게 된다. aligned 속성에는 2의 제곱수에 해당하는 숫자 만을
인자로 사용할 수 있으며 기본 정렬 제한보다 큰 값을 적용하는 경우에만 의미가 있다.
인자를 생략한 경우 실행 환경에 가장 적합한 정렬 제한을 가지도록 컴파일러가 결정한다.
반대로 packed 속성의 경우에는 정렬 제한을 가장 작은 수인 1로 줄이게 되며
1이 아닌 값을 적용하기 위해서는 #pragma를 이용할 수 있다.
위의 예제를 실행하면 다음과 같은 결과를 얻을 수 있다.
$ ./a.out
type size align address
struct asc 8 8 0x4020a8
struct asl 16 16 0x402080
struct pscl 9 1 0x40209a
struct pslc 10 2 0x402090
구조체에 비트 필드가 추가되는 경우 보다 복잡/미묘한 상황이 발생할 수 있는데
이에 대해서는 "더 이상의 자세한 설명은 생략한다".. ;;
'Info' 카테고리의 다른 글
Info - 유니코드와 멀티바이트 (0) | 2016.08.31 |
---|---|
Info - ASCII, ANSI, 유니코드, UTF-8 설명 (0) | 2016.08.31 |
Info - 구조체의 패딩비트 (0) | 2016.08.31 |
Info - 로딩 이미지 생성해주는 사이트 (0) | 2016.08.31 |
Info - static 헤더에 사용 불가 (0) | 2016.08.31 |