왜 컴파일러는 구조체의 메모리를 정렬해 놓을까?
- 적은 수의 컴파일러는 구조체의 필드를 메모리에 위치시킬 때, 중간 빈 공간 없이 쭉 이어서 할당한다. 하지만 대부분의 컴파일러는 성능 향상을 위해 cpu가 접근하기 쉬운 위치에 필드를 배치하는 데 이를 구조체 패딩이라고 한다. 그리고 중간 빈 공간에 들어간 것을 패딩 비트라고 한다.
- 참고로 os 32bit 환경에서는 4byte packing 방식이 빠르고 os 64bit 환경에서는 8byte packing 방식이 빠르다고 한다.
왜 빠를까?
- 패딩 비트가 없을 경우 어떤 일이 일어나는지 생각해보자. cpu는 메모리를 읽어올 때 한 번에 32bit os : 4byte 혹은 64bit os : 8byte를 읽어온다.
class Test
{
char _c1; // offsetof 0, size 1
int _i4; // offsetof 1, size 5
// 1byte packing을 하였다면
};
32 bit os에서는 int 변수를 읽기 위해서 먼저 2~4 byte에 위치한 메모리를 읽어야 한다. 그러므로 cpu는 첫 4byte의 메모리를 읽는다. 그리고 5byte에 위치한 나머지 int의 메모리를 읽기 위해 또 4byte의 메모리를 읽는다. 만약 패딩 비트로 char 뒤에 3byte가 채워져있었다면, int 변수를 읽기 위해 한 번만 메모리를 읽어도 될 것을 2번 읽게 된 것이다. 이런 식으로 구조체 메모리를 정렬해놓으면 쓸데없이 메모리를 읽는 것을 막기 때문에 성능 저하가 발생하지 않는다. Visual Studio 컴파일러 MSVC는 기본 packing 방식이 8byte로 지정되어 있다. (운영체제에 의존하는 방식이 아닌, 컴파일러에 의해 정해짐을 유의한다.)
아래와 같은 구조체가 선언되어있다.
struct box {
char c;
long long ll;
};
이때 long long 타입의 ll 멤버 변수를 읽고자 하면
32bit OS는 한 번에 4byte씩 메모리를 읽기 때문에 메모리에 3번 접근이 필요하고
64bit OS는 한번에 8byte씩 메모리를 읽기 때문에 메모리에 2번 접근이 필요하다.
하지만 아래와 같이 구조체에 패딩 값을 넣어준다면
패딩 비트의 크기만큼 메모리를 낭비하게 되지만
CPU가 ll을 읽어올 때 메모리에 접근하는 수가 줄어들어 성능 저하를 막을 수 있다.
(CPU 연산 횟수 감소)
어디에 주로 쓰일까?
- 네트워크를 통한 구조체 전송 시 구조체 패딩이 중요하다고 한다. (구조체를 그대로 직렬화 한 채 전송할 때)
- 서로 다른 컴퓨터 시스템에서 메모리를 읽는 방식이 다르기 때문에, packing 시 채워진 패딩 비트 때문에 각 컴퓨터에서 구조체를 다르게 읽을 수가 있기 때문이다. (그 시기에는 packing 정렬 방식을 1byte로 맞춰 놓는 방법으로 해결할 수 있다고 한다.)
- 사실 더 좋은 방법으로 DB Reflection을 통해, 변수들을 각각 대입한 뒤 직렬화한 후 전송하면 퍼포먼스도 향상시키고, 패딩 비트를 잘못 읽게 되는 경우도 해결할 수 있을 것 같다.
8 byte packing이라는 것은 무엇일까?
8 byte packing이라는 것은 구조체의 메모리 정렬 방식을 8의 배수로 맞추겠다는 게 아니라, 8 보다 큰 변수가 있을 경우 정렬을 포기하겠다는 뜻이다. 즉 만약 packing 크기를 4로 바꾼다면, double이나 long long 같은 타입들이 사용되었을 때는 더 이상 정렬을 보장하지 않게 된다. 그러므로 default packing 방식이 8인 것이다. 쉽게 말하면 64bit os 혹은 32bit os에서 cpu가 한 번에 읽을 수 있게 패딩 비트를 통해 정렬해준다.
#pragma pack(push, 4)
class Test4
{
public:
int _int4 = 0;
double _double8 = 1000000000000000001.;
double _double800[100]; // = 전부 이걸로 초기화 1000000000000000001.;
private:
virtual void init() { }
}
#pragma pack(pop)
#pragma pack(push, 8)
class Test8
{
public:
int _int4 = 0;
double _double8 = 1000000000000000001.;
double _double800[100]; // = 전부 이걸로 초기화 1000000000000000001.;
private:
virtual void init() { }
}
#pragma pack(pop)
void TestPack4()
{
Test4 test;
auto _struct = sizeof(test); // 820
auto _i4 = sizeof(test._int4); // 4
auto _d8 = sizeof(test._double8); // 8
auto _d800 = sizeof(test._double800); // 800
auto _i4_offset = offsetof(Test4, Test4::_int4); // 8 (앞의 8 바이트는 가상 함수 테이블)
auto _d8_offset = offsetof(Test4, Test4::_double8); // 12
auto _d800_offset = offsetof(Test4, Test4::_double800); // 20
double aa[100];
// 4 pack
std::chrono::system_clock::time_point startTime = std::chrono::system_clock::now();
for (int i = 0; i < 1000000000; i++) // or 10000000000
{
aa[i%100] = test._double800[i%100];
test._double800[i%100] = aa[i%100];
}
std::chrono::system_clock::time_point endTime = std::chrono::system_clock::now();
std::chrono::duration<double> defaultSec = endTime - startTime;
std::chrono::nanoseconds nano = endTime - startTime;
std::chrono::microseconds micro = std::chrono::duration_cast<std::chrono::microseconds>(nano);
std::chrono::milliseconds mill = std::chrono::duration_cast<std::chrono::milliseconds>(nano);
}
void Test1Pack8()
{
Test8 test;
auto _struct = sizeof(test); // 824
auto _i4 = sizeof(test._int4); // 4
auto _d8 = sizeof(test._double8); // 8
auto _d800 = sizeof(test._double800); // 800
auto _i4_offset = offsetof(Test8, Test8::_int4); // 8 (앞의 8 바이트는 가상 함수 테이블)
auto _d8_offset = offsetof(Test8, Test8::_double8); // 16 (_i4_offset 뒤의 4바이트가 패딩 비트로 들어감)
auto _d800_offset = offsetof(Test8, Test8::_double800); // 24
double aa[100];
// 8 pack
std::chrono::system_clock::time_point startTime = std::chrono::system_clock::now();
for (int i = 0; i < 1000000000; i++) // or 10000000000
{
aa[i%100] = test._double800[i%100];
test._double800[i%100] = aa[i%100];
}
std::chrono::system_clock::time_point endTime = std::chrono::system_clock::now();
std::chrono::duration<double> defaultSec = endTime - startTime;
std::chrono::nanoseconds nano = endTime - startTime;
std::chrono::microseconds micro = std::chrono::duration_cast<std::chrono::microseconds>(nano);
std::chrono::milliseconds mill = std::chrono::duration_cast<std::chrono::milliseconds>(nano);
// mill millsec로 테스트 진행
}
출처 : [개념] 구조체 패딩 (Struct Padding) (tistory.com)
출처 : https://wnsgml972.github.io/c/2019/11/21/c_struct_padding/
'프로그래밍 언어 > C++' 카테고리의 다른 글
C++ 클래스 접근 제한자 (Access Modifier) (0) | 2022.07.27 |
---|---|
C++ 스마트 포인터 (Smart Pointer) (0) | 2022.07.26 |
C++ 참조 대상 수 (Reference Counting) (0) | 2022.07.24 |
C++ 순환 참조 (Circular Dependency) & 데드락 (0) | 2022.07.24 |
C++ 4가지 타입의 캐스팅 (0) | 2022.07.22 |