#include <iostream>
class AAA
{
private:
int intA;
int intB;
double doubleA;
};
class BBB
{
private:
int intA;
double doubleA;
int intB;
};
int main()
{
AAA a;
BBB b;
std::cout << sizeof(a) << std::endl;
std::cout << sizeof(b) << std::endl;
return 0;
}
위의 코드는 무엇을 출력할까? AAA와 BBB는 메모리에서 몇 바이트를 차지할까?
둘 다 int 2개 double 1개를 가지고 있다. 그래서 둘 다 16바이트라고 생각할 수 있지만 그렇지 않다.
AAA는 예상대로 16바이트이지만 BBB는 24바이트이다. 왜일까?
메모리 패딩
컴파일러는 객체의 메모리를 할당할 때 메모리 공간에 padding을 넣어준다. (안 넣는 컴파일러도 있지만 현대에는 대부분 넣는다고 보면 된다.)
패딩을 넣는데는 몇 가지 규칙이 있는데 예를 들면 다음과 같은 식이다.
- 전체 메모리 크기는 필드에서 크기가 가장 큰 타입의 배수이다.
- 각 필드의 시작 지점은 해당 필드 크기의 배수여야 한다.
그렇다면 클래스 BBB의 메모리 레이아웃은 다음과 같을 것이다. (회색 음영부분이 padding된 공간)
왜 할까?
왜 이런식으로 메모리를 정렬해줄까? 이것은 cpu가 메모리에 접근하는 방법과 연관이 있다. 예를 들어 64bit OS는 한 번에 8Byte를 읽어온다.
그리고 위에서 언급했던 클래스 BBB의 메모리에 패딩이 없다고 가정해보자. 그렇다면 BBB의 메모리 구조는 아래와 같을 것이다.
이 상황에서 double 필드를 읽어온다고 가정하자. 64bit OS의 cpu는 첫 0~7Byte를 읽어올 것이다. 그리고 double의 나머지 4Byte를 읽어오기 위해 8~15Byte를 읽어온다. 즉 하나의 필드값을 읽는 데 메모리를 2번 읽는 것이다. (이 부분은 블로그를 참고한거라 정확한 동작 방식이 아닐 수 있지만, 느낌만 보자)
따라서 메모리가 정렬이 되어있다면 더 빠르게 동작할 것이다.
False Sharing 문제
병렬 프로그래밍을 할 때 발생할 수 있는 문제로 False Sharing이라는게 있다. 자세한건 따로 글을 작성해서 다루기로 하고 간단히 요약하자면, 현대의 CPU에는 여러 개의 코어가 들어있는데 각각의 코어에는 L1,L2,L3 캐시가 있다. 이 중 L1,L2 캐시는 각각의 코어마다 따로 가지고 있다.
이러한 캐시 메모리의 가장 작은 단위를 cache line 이라고 부르는데 이것은 일반적으로 64Byte의 크기를 가지고 있다.
BBB arr[100];
이제 여러 코어들이 BBB의 배열 arr에 접근한다고 가정하자. 코어 1은 0~63Byte에 , 코어2는 64~127Byte에 접근한다.
이때 BBB(2)에 문제가 생긴다. 비록 논리적인 메모리는 한개이지만 물리적으로는 두개의 캐시가 BBB(2)를 저장하고 있기 때문에 각 코어의 캐시 메모리를 Sync해주는 과정이 필요하게 된다. 그리고 이것은 큰 성능 저하를 발생시킨다.
이를 위해 C++에서는 alignas 키워드를 제공한다.
class alignas(32) BBB
{
private:
int intA;
double doubleA;
int intB;
};
int main()
{
BBB b;
std::cout << sizeof(b) << std::endl; //32 출력
}
저렇게 키워드를 사용하여 객체의 메모리 크기를 지정해 줄 수 있다. 위처럼 BBB를 32바이트로 설정하면 앞서 말했던 False Sharing 문제에서 자유로워 질 수 있다.
객체의 메모리 레이아웃 조정
프로그래머가 메모리 레이아웃을 직접 조절할 필요가 있을때가 또 있는데, 네트워크 스트림이나 파일 스트림에서 메모리를 특정 위치에 위치시켜야 하거나 unmanaged code를 사용하는 등의 경우이다.
방법은 앞서 말했던 alignas 말고도
#include <iostream>
#pragma pack(push, 2)
class DDD
{
private:
int intA;
double doubleA;
char charA;
};
#pragma pack(pop)
class alignas(32) EEE
{
private:
int intA;
double doubleA;
char charA;
};
int main()
{
DDD d;
EEE e;
std::cout << sizeof(d) << std::endl; // 14출력
std::cout << sizeof(e) << std::endl; // 32출력
return 0;
}
packing을 어떻게 해줄지 지정하는 등의 방법이 있다. 자세한건 그때 찾아보자.
C#의 경우 StructLayout 어트리뷰트를 이용하여 pack size와 layout 종류를 지정할 수 있다.
'이론 > 일반' 카테고리의 다른 글
N번째 난수 값 얻어오기 (0) | 2023.06.18 |
---|---|
클로저 (Closure) (3) | 2022.06.25 |
예외(Exception) 써야할까? (0) | 2022.06.19 |
멀티플레이 게임과 동기화 (0) | 2020.10.11 |