ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 메모리 정렬, 패딩 그리고 비트필드(feat. 구조체)
    개발/C·C++ 2024. 7. 7. 10:37

    컴퓨터는 극한의 효율을 추구해야 한다. 경쟁에서 우위를 점해야 하기도 하고 엔지니어 관점에도 같은 전기를 쓰면서 최대 성능과 최대 효율을 뽑는 것이 중요한 문제일 수밖에 없다.

     

    메모리 정렬에 몇 가지 규칙이 존재한다.

    1. 데이터가 저장되는 메모리 경계가 존재한다

    2. 1 바이트 데이터는 1 바이트 경계, 4 바이트 데이터는 4 바이트 경계

    3. 여기서 말하는 경계는 4바이트 데이터가 저장되는 시작 주소가 4의 배수인 것을 말한다

    4. 예를 들어 배열이 아닌 int형 데이터는 0x00000040(편의상 32비트 주소 체계 기준)에 저장된다

     

    메모리 정렬을 이야기할 때 데이터를 모아놓은 구조체가 자주 등장한다.

    다음의 두 구조체는 자료형의 총합은 같지만 실제 메모리 공간에서 차지하는 크기가 다르다.

    // 정렬되지 않은 구조체 예제
    struct UnalignedStruct {
        char a;        // 1바이트
        int b;         // 4바이트
        char c;        // 1바이트
        double d;      // 8바이트
    };
    
    // 정렬된 구조체 예제
    struct AlignedStruct {
        char a;        // 1바이트
        char c;        // 1바이트
        int b;         // 4바이트
        double d;      // 8바이트
    };

     

    먼저 정렬되지 않은 구조체 예제를 살펴보자. B는 바이트를 의미하고 padding(패딩)은 메모리 정렬을 위해 비워두는 비트를 의미한다.

     

    | char a(1B ) | padding(3B) | int b(4B) | char c(1B) | padding(7B) | double d(8B) -> 총합 24바이트

     

    시작이 char이 1바이트라고 해서 무작위의 메모리에 정렬을 시작하지 않는다. 64비트 체계에서는 최대한 8바이트 경계를 이용하고 동시에 각 자료형의 경계를 맞춰간다. 최대한이라고 표현한 이유는 char, short 같은 자료형은 필요에 따라 더 작은 정렬 단위로 처리될 수 있기 때문이다.

     

    위의 정렬되지 않은 구조체의 시작 주소를 0x0040이라고 하자. 편의상 16비트 주소 체계로 표현했다.

    1. char a는 0x0040이라는 시작 주소를 가지며 1바이트만 차지한다.

    2. int b는 0x0041에 위치할 수 없으므로 3bit의 패딩 비트 이후에 0x44에서 시작한다

    3. char c는 0x0048를 차지한다.

    4. double d는 0x0049에서 시작할 수 없으므로(10진수 환산 70), 가장 빠른 8바이트 경계인 0x0050에서 시작한다. 이 차이가 7bit의 패딩 비트다.

     

    이 맥락에서 정렬된 구조체 예제를 살펴보면 어렵지 않는다. 한 눈에 봐도 16바이트에 해당 자료들이 전부 담길 수 있다.

    | char a(1B) | char c(1B) | padding(2B) | int b(4B) | double d(8B) -> 총합 16바이트

     

    64비트 메모리 체계에선 메모리가 최대한 8바이트 단위로 정렬되기도 하지만 구조체에선 가장 큰 크기의 자료형 크기가 기준이 되기 때문에 어차피 8바이트 기준인 건 변함이 없다. 그 안에서 더 작은 자료형의 데이터들이 효율적으로 정렬된다.

     

    비트필드도 비슷한데 1바이트(8비트), 4바이트, 8바이트 단위 등으로 배치하면 효율적이게 된다.

    struct BitFieldExample {
        unsigned int a : 3; // 3비트 사용
        unsigned int b : 5; // 5비트 사용 (총 8비트, 1바이트 경계)
        
        unsigned int c : 4; // 4비트 사용
        unsigned int d : 4; // 4비트 사용 (총 8비트, 1바이트 경계)
        
        unsigned int e : 16; // 16비트 사용 (2바이트 경계)
        
        unsigned int f : 8; // 8비트 사용 (1바이트 경계)
    };

     

    비트필드를 사용할 때는 멤버 변수 자료형의 제한을 받는다.

    1. int

    2. unsigned int

    3. sigend int

    4. _Bool(C99부터)

    컴파일러에 따라 char, short. long 등도 사용할 수도 있지만 표준이 아니기 때문에 이식성에서 좋지 않다.

     

    다음 예제를 살펴보자.

    #include <stdio.h>
    
    typedef struct _DATAFLAG {
    	unsigned int top : 1;
    	unsigned int left : 2;
    	unsigned int bottom : 3;
    	unsigned int right : 2;
    } DATAFLAG;
    
    
    
    int main() {
    	DATAFLAG flag = { 0, 3, 7, 4 };
    
    	printf("%d\n", flag.top);
    	printf("%d\n", flag.left);
    	printf("%d\n", flag.bottom);
    	printf("%d\n", flag.right);
    	printf("===================\n");
    
    	printf("%X\n", *((unsigned int*)&flag));
    	printf("%zd\n", sizeof(flag));
    }

    right멤버 변수느 2비트만 쓸 수 있으므로 4를 표현할 수 없으니 0이 출력될 것이다. 이는 어렵지 않은데 16진수로 출력되는 부분을 보자. flag에 있는 값을 주소값을 unsigned int*로 캐스팅하여 다시 * 연산자를 이용해 unsigned int로 해석하게 출력하고 있다. 출력값이 3E인 이유를 살펴보자.

     

    구조체의 각 데이터를 8비트로 담아보자. 처음 시작이 오른쪽이다.

    1. 첫 비트는 0이므로 0을 넣느다

    2. 그다음 2비트는 3이므로 1 1을 채운다

    3. 그다음 3비트는 7이므로 1 1 1을 채운다

    4. 그다음 1비트는 4이므로 표현할 수 없어 0 0을 채운다

    | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | <- 시작

     

    16진수 한 자리는 4비트를 표현하고 있다. 4비트는 0 ~ 15를 표현한다. 그러므로 16진수 두 자리는 8비트는 표현하기 때문에 1바이트를 표현하기 좋다. 0x12는 한 바이트를 표현한다. 그럼 위의 형태를 1바이트로 표현하기 위해서는 8비트이니까 가운데를 기준으로 좌우 4비트씩 계산하면 된다

    | 0 | 0 | 1 | 1 |  -> 왼쪽(16진수의 두 번째 자리에 해당하므로 값을 10진수로 표현하면 3 * 16)

    | 1 | 1 | 1 | 0 | -> 오른쪽(16진수의 첫 번째 자 리에 해당하므로 값을 10진수로 표현하면 12 * 16^0) 

     

    이를 16진수로 표현하면 0x3E가 된다. E인 이유는 1 1 1 1이 F인 15이고 여기에서 1이 빠졌기 때문이다. E는 14다.

    댓글

Designed by Tistory.