이론/일반

객체 메모리, Object Alignment

tsyang 2021. 4. 18. 14:05

 

#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을 넣어준다. (안 넣는 컴파일러도 있지만 현대에는 대부분 넣는다고 보면 된다.)

 

패딩을 넣는데는 몇 가지 규칙이 있는데 예를 들면 다음과 같은 식이다.

 

  1. 전체 메모리 크기는 필드에서 크기가 가장 큰 타입의 배수이다.
  2. 각 필드의 시작 지점은 해당 필드 크기의 배수여야 한다.

그렇다면 클래스 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