struct member alignment

소개

struct Foo
{
    char a;
    int b;
};

위와 같은 구조체가 있다고 할 때 sizeof( Foo ) 는 어떤 값일까? 정답은 8 이다.

CPU 는 데이터를 처리할 때 cache line 에 메모리의 내용을 불러오는데, 이때 cache aligned 된 데이터 블럭을 가져오면 처리 효율이 높아진다. 따라서 만약 사용자가 별도로 지정하지 않는다면 컴파일러는 데이터 멤버 중 가장 큰 사이즈를 alignment 바운더리로 사용한다.

위 예에서는 32bit 플랫폼이라는 가정하에 char 가 1바이트, int 가 4바이트이므로 구조체의 멤버변수들은 모두 4바이트로 alignment 되어진다. 즉 char 멤버 변수 뒤에 3바이트가 padding1) 되어진다.

이는 User-defined struct 자료구조 (class, union 포함) 에만 해당하고, short, int, double, … 등의 built-in 자료형에는 해당되지 않는다.

alignment 규칙의 종류

위에서 설명한 alignment 는 컴파일 시점에 수행되는데, 이때 alignment 수치가 정해지는 규칙이 크게 세가지가 있다.

  1. struct 멤버 변수중 가장 큰 사이즈를 사용. (기본 alignment)
  2. struct 멤버 변수 중 가장 큰 사이즈와 프로젝트 세팅에서 지정한 수치 중 작은 녀석을 사용 (선택적 alignment)
  3. 위 모든 규칙을 무시하고 (지정 되었다 하더라도 overwrite 한다는 의미) 사용자가 지정한 사이즈를 사용 (절대적 alignment)

1. 기본 alignment

struct 멤버 변수중 가장 큰 사이즈를 사용

이것은 Default 규칙이다. 즉, 아무것도 손대지 않으면 이 규칙이 적용된다.

2. 선택적 alignment

struct 멤버 변수 중 가장 큰 사이즈와 프로젝트 세팅에서 지정한 수치 중 작은 녀석을 사용

이 규칙을 지정하는 방식은 크게 두가지가 있다. 프로젝트 속성에서 전역적으로 지정해주는 방식과, 소스코드에서 지역적으로 지정해주는 방식이 그것이다.

프로젝트 속성에서 전역적으로 지정

이는 VisualStudio 에 해당하는 사항이다.

프로젝트의 속성으로 들어가서 C/C++ → Code Generation → Struct Member Alignment 를 보면 Default 로 되어 있을 것이다.

Default 의 의미는 위 '1. 기본 alignment' 에서 언급한 대로 멤버 변수들 중 가장 큰 사이즈를 그 struct 의 alignment 수치로 사용한다는 뜻이다.

이제 Struct Member Alignment 필드를 수정하면 struct 멤버 변수 중 가장 큰 사이즈와 프로젝트 속성에서 명시적으로 지정한 alignment 수치 중 작은 녀석을 최종 alignment 수치로 사용한다.

위에 적은 struct Foo 를 예로 설명해보겠다.

만약 프로젝트 속성을 2바이트로 지정하면 sizeof( Foo ) 는 6 이 된다. 멤버 변수중 가장 큰 크기인 4바이트와 프로젝트 속성인 2바이트 중 작은 녀석인 2바이트가 최종 alignment 수치로 사용되기 때문이다. 이때 char a 변수 뒤에 1바이트의 padding 이 붙게 된다.

그리고 프로젝트 속성을 8바이트로 지정하면 sizeof( Foo ) 는 8 이 된다. 멤버 변수중 가장 큰 크기인 4바이트와 프로젝트 속성인 8바이트 중 작은 녀석인 4바이트가 최종 alignment 수치로 사용되기 때문이다. 이때 char a 변수 되에 3바이트의 padding 이 붙게 된다.

소스코드에서 지역적으로 지정

#pragma pack( # ) 을 이용한다. # 에는 power of 2 수치가 사용된다. 1, 2, 4, 8, 16, … 등.

#pragma pack( 2 )  // 2바이트 '선택적 alignment' 시작
struct Foo
{
    char a;
    int b;
};
#pragma pack() // '선택적 alignment' 끝. 기본값 사용

또는 아래와 같이 사용할 수도 있다.

#pragma pack( push, 2 )  // 2바이트 '선택적 alignment' 시작.
struct Foo
{
    char a;
    int b;
};
#pragma pack( pop ) // '선택적 alignment' 끝. 이전값 복원

후자는 소스코드 내에서 alignment 를 구역별로 스택처럼 사용할 수 있다는 의미다.

그리고 이 지역적 설정은 프로젝트에서 전역적으로 설정된 alignment 수치를 overwrite 한다.

3. 절대적 alignment

위 모든 규칙을 무시하고 (지정 되었다 하더라도 overwrite 한다는 의미) 사용자가 지정한 사이즈를 사용

이때는 __declspec( align( # ) ) 이 사용된다. # 에는 power of 2 수치가 사용된다. 1, 2, 4, 8, 16, … 등.

struct __declspec( align( 2 ) ) Foo
{
    char a;
    int b;
};

자, 이제 sizeof( Foo ) 는 무조건 6 이다. 위에 설명한 모든 규칙을 overwrite 해버린다.

alignment 규칙의 상속

이 alignment 속성은 Has-A 관계에 있는 데이터들 사이에 상속이 된다.

#pragma pack( 2 )
struct Foo
{
    char a;
    int b;
};
#pragma pack()
 
struct Foo2
{
    Foo c;
    char d;
};

위 예에서 sizeof( Foo2 ) 는 8 이다.

Foo 의 최종 alignment 속성은 2 이다.2) 그리고 Foo2 의 멤버변수 c 는 이 속성을 그대로 물려받아 2바이트 alignment 속성을 그대로 지닌다.

그리고 Foo2 의 입장에서 보면 2바이트의 c 와 1바이트의 d 중 큰 수치인 2 가 최종 alignment 수치로써 선택된다.

따라서 멤버변수 c 의 사이즈는 6바이트이고, 멤버변수 d 는 Foo2 의 최종 alignment 수치인 2 를 따라서 2바이트로 계산된다. 고로 sizeof( Foo2 ) 는 8 이 된다.

#pragma pack( 4 )
struct Foo
{
    char a;
    int b;
};
#pragma pack()
 
struct Foo2
{
    Foo c;
    char d;
};

같은 맥락에서 위 코드의 sizeof( Foo2 ) 는 12 가 된다.

그리고 지금까지 설명한 alignment 규칙의 상속은 __declspec( align( # ) ) 에서도 동일하게 적용된다.

alignment 병합

struct __declspec( align( 4 ) ) Foo
{
    char a;
    char b;
    int c;
};

위 코드에서 sizeof( Foo ) 는 8 이 된다. 컴파일러는 인접한 멤버 변수들이 하나의 alignment 단위에 들어가 있다면 이들을 합쳐서 계산한다. 멤버 변수 b 의 뒤에 2바이트의 padding 이 추가된다.

struct __declspec( align( 4 ) ) Foo
{
    char a;
    int c;
    char b;
};

하지만 위의 예에서 sizeof( Foo ) 는 12 가 된다. a 와 b 가 서로 인접하지 않기 때문에 다른 alignment 단위로 계산되어지기 때문이다. a 와 b 변수 뒤에 각각 3바이트의 padding 이 추가된다.

따라서 가급적 전자의 예와 같이 같은 타입의 변수들이 인접하도록 정렬해주는 것이 좋다.

언제 사용할까?

처음에 언급한 대로 CPU 는 aligned 된 데이터들에 대한 처리가 그렇지 않은 경우보다 더 빠르다. 따라서 특별한 일이 없다면 위에 소개한 alignment 변경은 하지 않는 것이 좋다.

alignment 변경을 고려해야 할 몇가지 경우를 소개하자면

메모리 사용의 최소화를 위해 alignment 를 사용할 수 있다. 이 경우 물론 1바이트 alignment 를 말하는 것일게다. 이렇게 할 경우 물론 깨알같은 메모리 절약을 할 수는 있겠지만 수행 성능은 보장되지 않는다. 따라서 아주아주 적은 양의 메모리 사용이라도 아쉬운 경우 alignment 수정을 고려해볼 수 있겠다.

또 다른 경우는 앞선 예와는 다르게 매우 민감한 경우이다. 다른 OS, 특히나 bit 수가 다른 플랫폼끼리의 파일데이터 공유 및 네트워크 송/수신이 이루어질 경우 심각한 오류를 유발할 수 있기에 alignment 를 반드시 사용해야 한다.

struct Foo
{
    char a;
    int b;
};
 
Foo foo;
 
FILE* pf = fopen( "some.file", "wb" );
fwrite( &foo, sizeof( Foo ), 1, pf );
fclose( pf );

이런식으로 기록된 파일을 다른 플랫폼에서 읽어들일 경우, 다른 alignment 의 사용으로 인해 심각한 오류가 발생할 수가 있다. 네트워크로 패킷을 송/수신할 경우도 마찬가지이다.

눈치 챘겠지만 sizeof( Foo ) 가 문제가 된다. 따라서 alignment 를 명시적으로 반드시 지정해줘야 한다. 보통은 alignment 1 로 세팅하게 될 것이다.

하지만 위 경우 (다른 플랫폼간 파일공유 및 패킷교환) 에서 alignment 를 사용하지 않고도 문제를 회피할 수 있는 방법이 있다. sizeof( Foo ) 와 같이 struct 단위로 복사가 일어나게 하지 않고 built-in 원소단위로 복사를 수행하면 된다.

FILE* pf = fopen( "some.file", "wb" );
fwrite( &foo.a, sizeof( char ), 1, pf );
fwrite( &foo.b, sizeof( int ), 1, pf );
fclose( pf );

위와 같이 말이다. 조금만 공을 들여 파일 및 네트워크 IO 에 대하여 struct 의 operator « (또는 ») 를 만들어주면 쉽게 관리할 수 있을 것이다.

참조

1) padding 은 0xCC 로 채워지게 된다.
2) Foo 의 멤버변수 중 가장 큰 사이즈인 4바이트와 지역적으로 지정된 2바이트 중 작은 수치인 2 로 선택