class MyClass {
public:
int i1 = 0; // 4 bytes
char c = 'a'; // 1 bytes
int i2 = 1; // 4 bytes
};
정답 12 bytes
메모리 패딩 (Memory Padding)
모든 것은 CPU님을 위하여
메모리 접근 횟수와 객체의 크기
CPU는 메모리를 읽을 때, OS가 지원하는 bit 크기에 따라 32bit OS에서는 4byte를 64bit OS에서는 8byte씩 읽어온다.
그럼 아래와 같은 코드가 존재한다고 가정하면 CPU는 어떻게 접근을 할까?
struct MyStruct {
long long ll; // 8 bytes
char c; // 1 bytes
};
32 bit OS
64 bit OS
4 bytes 씩 접근하여 총 3번 접근한다.
8 bytes 씩 접근하여총 2번 접근한다.
위 그림을 보면 CPU가 어떻게 메모리 값을 참고하는지 알 수 있다. 그럼 이런식으로 읽는 것이 왜 CPU에게 도움이 될까?
CPU가 메모리가 끝이 나는지 연산할 필요가 없기에 연산 속도가 상승한다.
CPU는 본인의 단어 크기만큼 읽기만 하면되고 데이터가 끝나는 영역을 따로 연산할 필요 없다.
캐시 메모리에 저장되는 포멧이 CPU가 사용하는 단어의 크기라면, 더욱 찾기 편해진다.
캐시 탐색을 정해진 규격 단위로 하면 오차가 발생하지 않고 추가적 연산이 필요가 없다.
단어 크기로 배정되어 있다면 접근하고 확인하기 편하다.마음대로 메모리에 할당된다면 CPU의 추가적 연산과 더불어 더 많은 메모리가 할당된다.
그렇다면 char는 1byte인데 나머지 부분은 어떻게 되는 것일까?
이때 찾아오는 것이 바로 메모리 패딩이라는 개념이다.
※ 지금부터 모든 테스트는 64bit 환경에서 동작한다고 가정한다.
📌 단어란? 컴퓨터 구조에서 '단어'란,CPU가 한번에 처리할 수 있는 블록을 말한다. 64 bit 환경에서는 8 bit씩 메모리를 읽을 수 있다고 한다면, 해당 CPU의 한 '단어'는 8bit인 것이다.
패딩 바이트 (Padding Byte)
메모리 패딩은CPU의 읽기 성능을 향상시키기 위해 CPU가 접근하기 쉬운 위치에 메모리를 배치하는 것이다.
구조체나 클래스는 내부에 선언된 변수들의 크기로 크기가 결정된다. 그런데 실제로 크기를 확인해보면 대부분내부 변수들 크기의 합보다 더 큰 공간이 할당되어 있는데, 이것이 바로패딩 바이트 때문에 그렇다.
패딩 바이트란 클래스나 구조체에 CPU가 접근하기 편하도록 공간 규약을 일정하게 잡는 것으로 메모리 사용량은 소 늘어나지만 Cache Hit Ratio가 증가하고 CPU 연산 횟수가 줄어든다.
📌 Cache Hit Ratio CPU는 레지스터와 메인메모리 외에 캐시 메모리 영역이 존재한다. 캐시 메모리는 레지스터처럼 바로 사용될 값도 아니지만 메인메모리까지 갔다오기엔 사용 빈도가 높은 데이터들을 빠르게 접근할 수 있도록 한다.
대부분의 메모리 접근은 특정한 위치의 근방에서 자주 일어나는 경향이 있기 때문에, 데이터를 크기는 작지만 속도가 빠른 캐시메모리에 복사해 두면 평균 메모리 접근 시간을 아낄 수 있다.
프로세서가 메인 메모리를 읽거나 쓰고자 할 때, 먼저 그 주소에 해당하는 데이터가 캐시에 존재하는지를 살핀다. 만약 그 주소의 데이터가 캐시에 있으면 데이터를 캐시에서 직접 읽고, 그렇지 않으면 메인 메모리에 직접 접근한다.
대부분의 프로세서는 메인메모리에 직접 접근해서 전송된 데이터를 캐시에 복사해 넣어, 다음번에 같은 주소에 프로세서가 접근할 때 캐시에서 직접 읽고 쓸 수 있도록 한다.
📌 offsetof 클래스나 구조체 등 자료들이 모인 컨테이너에각 멤버들이 할당되는 메모리 시작 위치를 반환한다.
Padding check
#include <iostream>
#include <stddef.h>
#include <stdlib.h>
using namespace std;
class MyClass {
public:
int i = 0; // 4 bytes
char c = 'a'; // 1 bytes
short s = 0; // 2 bytes
short s2 = 0; // 2 bytes
double d = 0; // 8 bytes
};
int main(void) {
MyClass mce;
cout << "Size : "<< sizeof(mce) << endl;
cout << endl;
cout << "Size of int : "<< sizeof(mce.i) << endl;
cout << "Size of Char : "<< sizeof(mce.c) << endl;
cout << "Size of Short : "<< sizeof(mce.s) << endl;
cout << "Size of Short2 : "<< sizeof(mce.s2) << endl;
cout << "Size of Double : "<< sizeof(mce.d) << endl;
cout << endl;
cout << "Offset of int : "<< offsetof(MyClass, i) << endl;
cout << "Offset of Char : "<< offsetof(MyClass, c) << endl;
cout << "Offset of Short : "<< offsetof(MyClass, s) << endl;
cout << "Offset of Short2 : "<< offsetof(MyClass, s2) << endl;
cout << "Offset of Double : "<< offsetof(MyClass, d) << endl;
return 0;
}
Size : 24
Size of int : 4
Size of Char : 1
Size of Short : 2
Size of Short2 : 2
Size of Double : 8
Offset of int : 0
Offset of Char : 4
Offset of Short : 6
Offset of Short2 : 8
Offset of Double : 16
변수
자료형 기본 크기
실제 크기
메모리 할당 크기
int i
4 byte
4 byte
0~3
char c
1 byte
1byte
4
short s
2 byte
2byte
6~7
short s2
2 byte
2byte
8~16
double d
8 byte
8byte
16~23
총계
17
17
24
위 코드를 보면, 각 변수들의 실제 크기와 메모리에 할당된 영역의 크기를 확인할 수 있다.
일반적 덧셈으로 MyClass 객체의 변수의 총합은 17 byte이다. 그런데 실제 메모리에 할당된 영역은 24 byte이다.
이는 아까 설명했다 싶이 64bit 환경에서는 CPU가 메모리를 8 bit 단위로 읽기 때문에 발생한 현상이며, 클래스에 선언된변수를 순서대로 메모리에 할당할 때, 8 bit 사이에 존재하는 데이터는 다음 단어로 넘겨버리는 것이다.
퀴즈) 다음 클래스의 매모리 크기는?
class MyClass {
public:
int i = 0;
char c = 'a';
short s = 0;
short s2 = 0;
char c2[6];
char c3;
double d = 0;
};
class MyClass {
public:
int i = 0; // 4 bytes
char c = 'a'; // 1 bytes
short s = 0; // 2 bytes
short s2 = 0; // 2 bytes
char c2[6]; // 6 bytes
char c3; // 1 bytes
double d = 0; // 8 bytes
};
Size : 32
Size of int : 4
Size of Char : 1
Size of Short : 2
Size of Short2 : 2
Size of Char2 : 6
Size of Char3 : 1
Size of Double : 8
Offset of int : 0
Offset of Char : 4
Offset of Short : 6
Offset of Short2 : 8
Offset of Char2 : 10
Offset of Char3 : 16
Offset of Double : 24
메모리 얼라인먼트(Memory Alignment)
모든 것은 메모리님을 위하여
자, 위 과정들을 통해 우리는 CPU가 빠른 연산을 위해 메모리에 할당되는 값들을 단어 단위로 나누어 관리한다는 것을 확인했다.
이 상황을 보고 "이런식으로 메모리가 사용되면 패딩 공간들이 너무 아깝지 않나?"라고 생각하는 당신은 한국인입니다.
만약 당신이 "슈퍼마리오 1"과 같이 굉장히 한정적인 메모리 용량을 가지고 게임을 만든다면, 당신에게는 1 byte, 1 bit가 굉장히 소중할 것이다.
극한의 메모리 사용량을 보여주는 게임
이런 어쩔 수 없는 극한의 상황이라면 당신에게 메모리 패딩은 독이될 뿐이다. 이때, 당신이 할 수 있는 것이 메모리 얼라이어먼트이다.
PACK
C++에는 구조체나 클래스의 멤버 변수의 크기를 정렬할 수 있는 pack이라는 전처리기 명령어가 존재한다.
상위 전처리 명령어
#pragma
전처리 명령어
pack
구조체나 클래스의 멤버 변수의 정렬 규칙을 설정하는 데 사용되는 전처리 지시어
매개변수
push
현재 정렬 설정을 스택에 저장(push)
int n
새로운 정렬 크기를 설정
pop
스택에 이전 저장한 정렬 설정을 복원
pack(push, 1)
#pragma pack(push, 1)
class MyClass {
public:
int i = 0; // 4 bytes
char c = 'a'; // 1 bytes
short s = 0; // 2 bytes
short s2 = 0; // 2 bytes
double d = 0; // 8 bytes
};
#pragma pack(pop)
MyClass Size : 17
Size of int : 4
Size of Char : 1
Size of Short : 2
Size of Short2 : 2
Size of Double : 8
Offset of int : 0
Offset of Char : 4
Offset of Short : 5
Offset of Short2 : 7
Offset of Double : 9
우리는 앞서 64 bit 환경의 기본 메모리 패딩은 8 bit인 것을 학습했다. 그런데 어째서 MyClass의 크기가 17byte가 나올까?
이는 예제로 사용된 pack 전처리 지시어 을 통해 패딩의 크기를 1로 재조정하여 그렇다.
패딩의 크기가 1이라면 사실상 모든 객체가 정확히 자신이 할당되어야 하는 크기만큼 메모리에 할당된다.
그럼 패딩크기를 2로 설정했다면 어떻게 될까?
pack(push, 2)
#include <iostream>
#include <stddef.h>
#include <stdlib.h>
using namespace std;
#pragma pack(push, 2)
class MyClass {
public:
int i = 0; // 4 bytes
char c = 'a'; // 1 bytes
short s = 0; // 2 bytes
short s2 = 0; // 2 bytes
double d = 0; // 8 bytes
};
#pragma pack(pop)
int main(void) {
MyClass mce;
cout << "Size : "<< sizeof(mce) << endl;
cout << endl;
cout << "Size of int : "<< sizeof(mce.i) << endl;
cout << "Size of Char : "<< sizeof(mce.c) << endl;
cout << "Size of Short : "<< sizeof(mce.s) << endl;
cout << "Size of Short2 : "<< sizeof(mce.s2) << endl;
cout << "Size of Double : "<< sizeof(mce.d) << endl;
cout << endl;
cout << "Offset of int : "<< offsetof(MyClass, i) << endl;
cout << "Offset of Char : "<< offsetof(MyClass, c) << endl;
cout << "Offset of Short : "<< offsetof(MyClass, s) << endl;
cout << "Offset of Short2 : "<< offsetof(MyClass, s2) << endl;
cout << "Offset of Double : "<< offsetof(MyClass, d) << endl;
return 0;
}
Size : 18
Size of int : 4
Size of Char : 1
Size of Short : 2
Size of Short2 : 2
Size of Double : 8
Offset of int : 0
Offset of Char : 4
Offset of Short : 6
Offset of Short2 : 8
Offset of Double : 10
pack이 2로 설정된다면, char형에게 1개의 패딩이 생긴 것을 확인할 수 있다.
이는 char 다음에 오는 변수 'short s'와 'short s2'가 메모리 자리를 2개 차지하기에 한 칸의 패딩이 생긴 것이다.
📌 pack 비트 pack 명령어로 정렬 크기를 선언할 때, C++는 기본적으로 2의 제곱근 값들만 정의하기를 권장한다. 이를 어길시, warning문을 출력하게 된다.
alignof
alignof는 살펴보려는 값의 정렬 크기가 얼마로 설정되어 있는지 확인하는 함수이다.
#include <iostream>
int main() {
alignas(16) char alignedCharArray[24];
// alignof를 사용하여 배열의 정렬 크기 출력
std::cout << "Alignment of alignedCharArray: " << alignof(decltype(alignedCharArray)) << std::endl;
// 다양한 타입에 대한 정렬 크기 출력
std::cout << "Alignment of int: " << alignof(int) << std::endl;
std::cout << "Alignment of double: " << alignof(double) << std::endl;
std::cout << "Alignment of struct { char a; int b; }: " << alignof(struct MyStruct { char a; int b; }) << std::endl;
return 0;
}
Alignment of alignedCharArray: 16
Alignment of int: 4
Alignment of double: 8
alignas
pack이 클래스와 구조체의 정렬 크기를 결정한다면, alignas는 변수나 타입의 정렬 크기를 일일이 지정할 수 있게 한다.
#include <iostream>
using namespace std;
alignas(16) char alignedCharArray[24]; // 16바이트로 정렬
struct alignas(8) AlignedStruct { int a; double b; }; // 8바이트로 정렬
int main(void) {
AlignedStruct mce;
cout << "Size CharArray : "<< sizeof(alignedCharArray) << endl;
cout << "Size AlignedStruct : "<< sizeof(mce) << endl;
cout << endl;
cout << "Align of alignedCharArray : "<< alignof(alignedCharArray) << endl;
cout << "Align of AlignedStruct : "<< alignof(mce) << endl;
cout << endl;
cout << "Offset of int : "<< offsetof(AlignedStruct, a) << endl;
cout << "Offset of double : "<< offsetof(AlignedStruct, b) << endl;
return 0;
}
Size CharArray : 24
Size AlignedStruct : 16
Align of alignedCharArray : 16
Align of AlignedStruct : 8
Offset of int : 0
Offset of double : 8
위 코드를 봤을 때, 이상한 점을 발견했다면 당신은 굉장히 눈썰미가 좋은 사람이다.
char배열인 'alignedCharArray' 변수를 보자. 분명 변수의 크기는 24이고 char형이기에 메모리는 24를 할당 받는다. 그런데 우리는 이번 실험에서 alignas로 정렬의 크기를 16으로 설정했다.
앞서 배운 것을 토대로 생각한다면 "배열의 정렬 크기를 16으로 설정하면 24 크기의 배열은 32개의 메모리를 할당해야하지 않나?"라고 생각할 수 있다.
하지만, 실제 alignas는 말 그대로 "정렬의 크기"만 설정하는 것이지 실제 값의 크기를 변경하지 못한다.
하여 다음과 같이 자료형의 기본 크기보다 줄이거나(설정 값만 적용되고 실제 값은 유지된다), 구조체와 클래스가 가진 가장 큰 자료형 이상의 값은 줄 수 없다(무시된다).
#include <iostream>
using namespace std;
alignas(4) double d; // 4바이트로 정렬
struct alignas(4) AlignedStruct { int a; double b; }; // 4바이트로 정렬
int main(void) {
cout << "Size d : "<< sizeof(d) << endl;
cout << "Align of d : "<< alignof(d) << endl;
cout << endl;
AlignedStruct mce;
cout << "Size AlignedStruct : "<< sizeof(mce) << endl;
cout << "Align of AlignedStruct : "<< alignof(mce) << endl;
cout << "Offset of int : "<< offsetof(AlignedStruct, a) << endl;
cout << "Offset of double : "<< offsetof(AlignedStruct, b) << endl;
return 0;
}
Size d : 8
Align of d : 4
Size AlignedStruct : 16
Align of AlignedStruct : 8
Offset of int : 0
Offset of double : 8
📌 alignas 크기 alignas를 사용할 때는 pack과 다르게 2의 제곱근이 아니면 오류가 발생한다.