[Go] 메모리 친화적인 구조체 작성하기

Hoplin·2023년 11월 19일
0

구조체 간단히 살펴보기

구조체는 여러 필드를 묶어 하나의 타입으로 사용하게 해줄 수 있습니다. 가장 기본적인 형태를 간단히 살펴보고 넘어가겠습니다.

type Student struct {
	Name  string
	Class int
	No    int
	Score int
}

Student 라는 구조체를 생성했습니다. 이 안에는 Name, Class, No, Score라는 필드가 있으며 Student타입의 변수를 초기화하면 아래와 같이 작성할 수 있습니다.

func main() {
	// 모든 필드 초기화
	student1 := Student{"Hoplin", 1, 1, 100}
	// 특정 필드 지정하여 초기화
	student2 := Student{Name: "Hoplin2", Score: 100}
}

JS/TS 계열에서 봤던 일반 Object(JSON)와 비슷한 면이 있습니다. 구조체 안의 구조체를 만들 수 도 있죠.

package main

type User struct {
	Name  string
	ID    string
	Age   int
	Level int
}

type VIPUser struct {
	UserInfo *User
	Level    int
	Price    int
}

func main() {
	user := &User{"Hoplin", "a", 1, 1}
	vip := &VIPUser{user, 10, 20}
}

다만 주의할점은 구조체는 대입했을때 Call by Reference 가 아닌 Call by Value 가 되기 때문에 이점은 주의해야합니다.(물론 위 예시에서 User의 주소는 공유되지만요)

	vip2 := vip
	fmt.Printf("%f", &vip) // %!f(**main.VIPUser=0x140000ac018) 
	fmt.Printf("%f", &vip2) // %!f(**main.VIPUser=0x140000ac020)
}

구조체 필드 순서에 따라 구조체의 크기는 변합니다.

이 글의 주제인 메모리 친화적인 구조체라는 주제의 발단을 살펴보겠습니다.

type User struct {
	Age   int32
	Score float64
}

이 구조체의 경우에는 자료형만 봤을때 int32 4바이트, float64 8바이트 총 12바이트의 크기를 가져야합니다. 하지만 실제 메모리 크기를 출력해보면 다른 결과가 나옵니다.

Go에서 메모리 크기를 출력하기 위해서는 unsafe.Sizeof() 를 활용합니다

func main() {
	user1 := User{25, 90}
	fmt.Println(unsafe.Sizeof(user1)) //16
}

출력을 하면 16이라는 값이 나오는것을 볼 수 있습니다. 이는 메모리 정렬 (Memory Alignment) 이라는 현상에 의해 발생하는것입니다.

관련 Reference: https://go101.org/article/memory-layout.html

메모리 정렬

메모리 정렬 이란 컴퓨터가 데이터에 효과적으로 접근하고자 하는 메모리를 일정 크기의 간격으로 정렬하는것을 의미합니다. 일반적으로 32비트, 64비트 컴퓨터라고 불리는것이 있습니다.

32비트 컴퓨터는 4바이트의 레지스터 크기를, 64비트 컴퓨터는 8바이트의 레지스터 크기를 가지는 컴퓨터를 의미하며 레지스터의 크기는 곧 한번에 연산할 수 있는 크기를 의미합니다. 데이터가 레지스터의 크기와 똑같은 크기로 정렬되어있으면 더 효율적으로 데이터를 불러올 수 있다는 의미입니다. 64비트의 컴퓨터에서는 64,72와같이 8의 배수인 메모리 주소에 데이터를 할당하게 됩니다.

다시 위에서 보았던 구조체를 가져와 보겠습니다.

type User struct {
	Age   int32
	Score float64
}

예를들어 구조체를 메모리에 저장할때 64를 시작주소로 두었다면, Age 저장 후 Score 의 시작 주소는 68이 될것입니다(int32 타입은 4바이트입니다.) 하지만 68은 8의 배수가 아니므로, 읽기성능의 손해가 발생합니다.

그리고 위 배치가 우리가 User 라는 구조체의 크기가 12바이트일것이라 예상했던 이유입니다. 하지만 실제로 저장될때는 읽기성능의 손해를 없애기 위해 아래와같이 저장됩니다.

보면 Score의 시작주소를 8의배수인 72에 맞추기 위해 필드 사이의 공간을 띄운것을 볼 수 있습니다. 그리고이러한 부분은 Memory Padding 이라고 부릅니다. 그리고 이러한 현상이 발생하여 실제로 크기를 출력했을때 16바이트가 출력된 것입니다.

메모리 친화적인 구조체

결국 메모리 패딩이 발생하는것은 불필요한 메모리 점유 공간을 만드는 것입니다. 이를 방지하기 위해서는 구조체 필드 배치시 메모리 패딩이 발생하지 않도록 배치하는 방법이 있습니다. 아래와같은 구조체가 있다고 가정하고 구조체 크기를 출력해보겠습니다.

type Example struct {
	A int8
	B int64
	C int8
	D int64
	E int8
}

func main() {
	strt := Example{1, 2, 3, 4, 5}
	fmt.Println(unsafe.Sizeof(strt)) // 40
}

40 바이트가 나오는것을 볼 수 있습니다. 메모리에 저장되는 형태를 그려보면 아래와 같이 저장되게 됩니다.

1바이트의 크기를 가진 int8 타입 필드들은 모두 7바이트 크기의 메모리 패딩을 가지게 되는것입니다. 이러한 메모리 낭비를 줄이는 방식은 간단합니다. 크기가 작은 필드들끼리 묶어서 배치를 하는것입니다. (물론 크기가 각각 다른 구조체가 오게 되면 계산을 하는것이 좋습니다)

type Example struct {
	D int64
	B int64
	C int8
	A int8
	E int8
}

func main() {
	strt := Example{1, 2, 3, 4, 5}
	fmt.Println(unsafe.Sizeof(strt)) // 24
}

24바이트로 총 16바이트의 크기를 절약할 수 있습니다.

profile
더 나은 내일을 위해 오늘의 중괄호를 엽니다

0개의 댓글