
고(Go)에서 빈 구조체의 수수께끼: 사용법과 최적화를 이해해보세요
일반적으로 구조체는 메모리 블록을 차지합니다. 그러나 특별한 경우가 있습니다: 만약 빈 구조체라면, 크기는 제로입니다. 이것이 어떻게 가능한 것이고, 빈 구조체의 사용은 무엇일까요?
type Test struct {
A int
B string
}
func main() {
fmt.Println(unsafe.Sizeof(Test{}))
fmt.Println(unsafe.Sizeof(struct{}{}))
}
/*
24
0
*/
빈 구조체의 비밀
특별한 변수: zerobase
빈 구조체는 메모리 크기가 없는 구조체입니다. 이 문장은 맞지만, 더 정확하게 말하면 특별한 시작점이 있습니다: zerobase 변수입니다. 이는 8바이트를 차지하는 uintptr 전역 변수입니다. 무수히 많은 구조체 '' 변수가 정의될 때, 컴파일러는 이 zerobase 변수의 주소를 할당합니다. 다시 말해, Go에서 크기가 0인 모든 메모리 할당은 동일한 주소 &zerobase 를 사용합니다.
예제
package main
import "fmt"
type emptyStruct struct{}
func main() {
a := struct{}{}
b := struct{}{}
c := emptyStruct{}
fmt.Printf("%p\n", &a)
fmt.Printf("%p\n", &b)
fmt.Printf("%p\n", &c)
}
// 0x58e360
// 0x58e360
// 0x58e360
빈 구조체 변수의 메모리 주소는 모두 동일합니다. 이는 컴파일 시에 특수한 형식의 메모리 할당을 만나면 컴파일러가 &zerobase를 할당하기 때문입니다. 이 로직은 mallocgc 함수에 있습니다.
//go:linkname mallocgc
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
if size == 0 {
return unsafe.Pointer(&zerobase)
}
...
이것이 빈 구조체의 비밀입니다. 이 특수한 변수로 많은 기능을 수행할 수 있습니다.
빈 구조체와 메모리 정렬
일반적으로 빈 구조체가 더 큰 구조체의 일부인 경우 메모리를 차지하지 않습니다. 그러나 빈 구조체가 마지막 필드인 경우에는 메모리 정렬이 트리거됩니다.
예시
type A struct {
x int
y string
z struct{}
}
type B struct {
x int
z struct{}
y string
}
func main() {
println(unsafe.Alignof(A{}))
println(unsafe.Alignof(B{}))
println(unsafe.Sizeof(A{}))
println(unsafe.Sizeof(B{}))
}
결과:
8
8
32
24
포인터가 필드를 가리키는 경우, 반환된 주소는 구조체 외부에 있을 수 있으며, 구조체가 해제될 때 해당 메모리가 해제되지 않으면 메모리 누수로 이어질 수 있습니다. 따라서 다른 구조체의 마지막 필드로 빈 구조체가 사용될 때는 안전을 위해 추가적인 메모리가 할당됩니다. 빈 구조체가 시작이나 중간에 위치할 경우, 그 주소는 다음 변수와 동일합니다.
type A struct {
x int
y string
z struct{}
}
type B struct {
x int
z struct{}
y string
}
func main() {
a := A{}
b := B{}
fmt.Printf("%p\n", &a.y)
fmt.Printf("%p\n", &a.z)
fmt.Printf("%p\n", &b.y)
fmt.Printf("%p\n", &b.z)
}
/**
0x1400012c008
0x1400012c018
0x1400012e008
0x1400012e008
**/
빈 구조체의 사용 사례
빈 구조체(struct{})의 존재 이유는 메모리를 절약하는 데 있습니다. 내용에 관심이 없지만 구조체가 필요할 때 빈 구조체를 사용하는 것을 고려해보세요. Go의 핵심 복합 구조체인 map, chan 및 slice 모두 빈 구조체를 사용할 수 있습니다.
맵과 구조체
// 맵 생성
m := make(map[int]struct{})
// 값 할당
m[1] = struct{}{}
// 키 존재 여부 확인
_, ok := m[1]
채널과 구조체
채널과 구조체를 결합하는 클래식한 시나리오에서는 구조체를 신호로 사용하고 내용에 대해 신경 쓰지 않습니다. 이전 글에서 분석한 대로 채널의 필수 데이터 구조는 관리 구조체와 링 버퍼가 함께합니다. 구조체를 요소로 사용하는 경우 링 버퍼는 제로 할당됩니다.
chan과 struct를 함께 사용하는 유일한 목적은 빈 구조체 자체가 어떤 값을 전달할 수 없기 때문에 신호 전달에 사용된다는 것입니다. 일반적으로 버퍼가 없는 채널과 함께 사용됩니다.
// 신호 채널 생성
waitc := make(chan struct{})
// ...
goroutine 1:
// 신호 전송: 요소 추가
waitc <- struct{}{}
// 신호 전송: 종료
close(waitc)
goroutine 2:
select {
// 신호 수신 및 해당 작업 수행
case <-waitc:
}
이 시나리오에서 struct가 꼭 필요할까요? 정말 필요하지는 않으며, 절약되는 메모리는 미미합니다. 핵심은 chan의 요소 값이 중요하지 않다는 것이며, 따라서 struct가 사용됩니다.
요약
- 빈 구조체는 여전히 크기가 0인 구조체입니다.
- 모든 빈 구조체는 동일한 주소를 공유합니다: zerobase 주소입니다.
- 빈 구조체의 메모리를 차지하지 않는 기능을 활용하여 맵을 사용하여 집합 및 채널을 구현하는 등 코드를 최적화할 수 있습니다.
참고 자료
- 빈 구조체, Dave Cheney
- Go 최종 릴리스 - struct'' 빈 구조체가 정확히 무엇인가요?