Digital Recipe
구조체의 최적화 (구조체 정렬) 본문
개발 언어를 이용하여 개발 시 효율적인 데이터 관리를 위해 구조체와 같은 새로운 자료형을 선언하여 사용하게 된다. 하지만 이 자료형을 어떻게 구성하느냐에 따라 성능과 메모리 측면에서 효율성 차이를 가지게 된다. 이 게시글에서는 C언어 구조체를 기준으로 성능과 메모리 측면에서 어떻게 최적화 할 수 있는지 알아보도록 하겠다.
이 두 가지 측면에 접근하기 위해서는 Data Alignment와 Data Structure Padding에 대해서 알아야 한다. Data Alignment는 성능 향상을 위한 효율적인 처리를 위해 데이터를 정렬하는 것에 대한 이야기이다. 그리고 Data Structure Padding은 컴파일러가 정의된 구조체를 어떻게 다루는지에 대한 이야기로 Data Alignment이라는 조건을 만족하기 위한 구조체를 구성하기 위한 방법이다.
1. 데이터 정렬(Data Alignment)
현대의 컴퓨터는 메모리에 데이터를 쓰거나 읽을 때 워드(WORD)라는 단위로 수행한다. 그리고 32Bit 컴퓨터에서 1WORD는 4바이트(Bytes)의 크기를 가진다. 그러면 Data Alignment는 무엇을 의미하는 것일까? 컴퓨터는 메모리를 4바이트 단위로 접근하고 읽고 쓴다.
그러면 하나의 예시로서 데이터가 메모리 형태에 맞게 정렬되지 않은 경우를 살펴보자. 하나의 6바이트짜리 데이터가 있고 0x03부터 0x08까지 필요한 데이터가 저장되어 있다. 컴퓨터는 이 데이터를 읽기 위해 몇 번의 읽기 연산이 필요할까? 정답은 3번이다. 0x00부터 4바이트, 0x04부터 4바이트, 0x08부터 4바이트, 총 12바이트 만큼 데이터를 가져올 필요가 있다.
[그림01] 메모리 정렬
반대로 데이터 정렬이 맞는 경우를 살펴보자. 6바이트 데이터가 0x04부터 0x09까지 저장되어 있다. 컴퓨터는 이 데이터를 가져오기 위해 0x04에서 1번, 0x08에서 1번, 총 2번의 읽기 연산을 통해서 8바이트 만큼 읽어오게 된다. 이것이 메모리 구조에 맞춰 데이터를 정렬함으로 얻을 수 있는 성능향상의 측면이다.
그러면 데이터 정렬을 맞춰기 위해서 구조체를 어떻게 구성해야 할까? 방법은 단순하다. 구조체의 크기를 WORD의 배수 크기로 맞추면 된다. 즉 일반적인 상황에서는 4바이트의 배수 크기로 구성하면 된다. 하지만 구조체를 만들고 실제 컴파일을 해보면 의도했던 구조체의 크기가 아닌 경우가 있다. 이는 컴파일러가 구조체를 다루는 방법을 통해서 확인할 수 있다.
2. 데이터 구조 패딩 (Data Structure Padding)
구조체의 크기를 메모리 처리 단위에 맞게 구성하기 위해서는 개발자가 명시적으로 의미없는 데이터를 추가해서 크기를 맞추면 된다. 하지만 그 전에 컴파일러가 구조체를 컴파일하면서 암묵적인 의미없는 데이터 추가를 수행하는데 이 규칙을 이해해야 얼마 만큼의 명시적인 의미없는 데이터를 추가할지 결정할 수 있다.
컴파일러가 암묵적으로 의미없는 데이터를 추가하는 규칙은 다음과 같다.
(1) 구조체의 크기는 구조체를 이루는 자료형 중 가장 크기가 큰 자료형의 배수가 된다.
(2) 구조체를 이루는 자료형 중 가장 크기가 큰 자료형으로 데이터를 정렬한다.
이제 예제를 통해 위 규칙을 살펴보자.
CASE 1 :
struct MyData
{
short Data1;
short Data2;
short Data3;
}
CASE 1을 살펴보자. 가장 큰 자료형은 short(2바이트)이다. 그러므로 이 구조체의 크기는 2바이트의 배수가 된다. 그리고 short 자료형에 맞게 3개의 변수가 다 정렬된다. 그래서 이 구조체의 크기는 6바이트가 된다. 그러면 개발자는 명시적으로 2바이트의 의미없는 데이터를 추가할 필요가 있다. 그렇게 코드를 바꿔본다면 아래와 같이 변경될 수 있다.
struct MyData
{
short Data1;
short Data2;
short Data3;
char temp[2];
}
CASE 2 :
struct MixedData
{
char Data1;
short Data2;
int Data3;
char Data4;
};
다음으로 CASE 2를 살펴보자. 가장 큰 자료형은 int(4바이트)이다. 그러므로 이 구조체의 크기는 4바이트의 배수가 된다. 그리고 char(1바이트)와 short(2바이트)는 4바이트 단위 정렬에 함께 포함시킬 수 있다. 하지만 이어지는 int까지 4바이트 단위 정렬에 포함하기에는 남은 공간이 부족하다. 그럼 컴파일러는 short와 int 사이에 암묵적인 패딩 1바이트를 넣고 하나의 4바이트 단위 구성을 마무리한다. 그리고 새로운 4바이트 단위를 구성하여 int를 포함시킨다. 그리고 남은 char도 또 새로운 4바이트 단위 공간에 포함시키고 남은 3바이트를 암묵적 패딩으로 채우고 마무리 한다. 그럼 이 구조체의 크기는 12바이트가 될 것이다.
이를 코드 형태로 풀어 본다면 아래와 유사할 것이다.
struct MixedData
{
/* 첫 번째 4바이트 공간 */
char Data1; // 1바이트
short Data2; // 2바이트
char temp1[1] // 1바이트 - 암묵적 패딩
/* 두 번쨰 4바이트 공간 */
int Data3; // 4바이트
/* 세 번째 4바이트 공간*/
char Data4; // 1바이트
char temp2[3] // 3바이트 - 암묵적 패딩
};
마무리로 이런 규칙으로 구조체 내 자료형의 선언 순서에 따라 구조체의 크기가 달라질 수 있다. 구조체 내 자료형은 동일하지만 선언 순서가 달라지는 경우를 살펴보자.
CASE 3-1
struct newData
{
char ch1;
double double_data;
char ch2;
}
CASE 3-2
struct newData
{
char ch1;
char ch2;
double double_data;
}
CASE 3-1와 CASE 3-2는 동일한 역할을 하는 구조체이다. 하지만 구조체의 크기는 다르다. 위에 컴파일러에 의한 패딩 결과물로 인해 CASE 3-1은 24바이트, CASE 3-2는 16바이트의 크기를 가진다.
그럼 이제 효율적인 구조체를 선언하기 위한 방법을 정리해 보자. 먼저 구조체 안의 자료형 선언 순서를 가장 큰 자료형의 크기에 맞춰 최대한 포함시켜 컴파일러에 의한 패딩을 최소화(예시 CASE 3-2) 한다. 그렇게 구성된 구조체의 크기가 1WORD의 배수가 아니라면 구조체 안에 의미를 가지지는 않지만 크기를 맞추기 위한 자료형을 함께 선언(예시로 CASE 1 개선 후 코드)한다.
REFERENCE
[01] https://en.wikipedia.org/wiki/Data_structure_alignment
Written By Hoseok Seo
2016. 08. 25