『Tucker의 Go 언어 프로그래밍』 스터디 요약 노트입니다.
14장. 포인터
포인터의 Go 언어에서의 특징만 확인해보자.
- 빈 포인터의 표기는
null
이 아니라nil
로 표현한다. - 구조체를 가리키는 포인터에서 해당 구조체의 필드 접근은
->
이 아니라.
으로 가능하다. call-by-address
를 통해 함수의 인자를 전달, 수정할 수 있다.new()
를 사용하여 인스턴스를 생성할 수 있다.- 인스턴스를 가리키는 포인터가 없으면 가비지 컬렉터가 메모리를 회수한다.
- go 컴파일러에서 탈출 분석을 확인하여, 지역변수가 콜 스택이 아니라 힙 영역에 존재할 수 있다.
기본적인 포인터 사용
|
|
$ ./go_pointer_basics
&data1: 0x140000b2000
pData1: 0x140000b2000, &pData1: 0x140000ac018
pData2: 0x140000b2060, &pData2: 0x140000ac020
pData3: 0x140000b20c0, &pData3: 0x140000ac028
Call-by-value &data: 0x140000b2120
{0 [0 0 0 0 0 0 0 0 0 0]}
Call-by-address &data: 0x140000b2000
{10 [1 0 0 0 0 0 0 0 0 0]}
Call-by-value &data: 0x140000b2240
&{10 [1 0 0 0 0 0 0 0 0 0]}
Call-by-address &data: 0x140000b2000
&{20 [2 0 0 0 0 0 0 0 0 0]}
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x10044fe78]
goroutine 1 [running]:
main.main()
/Workspace/go/src/musthavego/ch14/go_pointer_basics.go:32 +0x408
가비지 컬렉터 순환 참조
각 인스턴스를 가리키는 포인터가 없을때 해당 변수는 가비지 컬렉션에 의해 메모리가 회수될 수 있다고 한다.
그렇다면 순환 참조를 해서 각 인스턴스를 가리키는 포인터는 존재하지만, 해당 순환을 가리키는 변수가 없는 경우에도 가비지 컬렉션이 잘 동작하는지 확인해봤다.
|
|
직접 실행해 본 결과, 딱히 메모리 누수가 발생하지 않는 것으로 보였다. 하지만 컴파일러가 최적화 과정에서 해당 요소를 미리 차단했는지는 확실하지 않아서, 이 부분은 컴파일 옵션, 메모리 누수 확인 툴을 좀 더 공부해서 다시 확인해봐야 할 것 같다.
탈출 분석
go 컴파일러는 탈출 분석을 하여 지역 변수의 주소를 return
해도 오류가 나지 않는다. (해당 변수를 heap에 할당한다.)
해당 부분을 검증하기 위해 직접 실험해보았다. 실험 결과 모든 변수는 자체적인 goroutine의 heap에 저장되는 것 같다.
|
|
$ dlv exec ./go_mem_layout_struct
Type 'help' for list of commands.
(dlv) b go_mem_layout_struct.go:32
Breakpoint 1 (enabled) set at 0x499278 for main.main() ./go_mem_layout_struct.go:32
(dlv) continue
pData1: 0xc000102060
pData2: 0xc0001020c0
pData3: 0xc000102120
pData4: 0xc000102180
pData5: 0xc0001021e0
> main.main() ./go_mem_layout_struct.go:32 (hits goroutine(1):1 total:1) (PC: 0x499278)
Warning: debugging optimized function
29: fmt.Printf("pData4: %p\n", pData4)
30: fmt.Printf("pData5: %p\n", pData5)
31:
=> 32: for {
33: // Force infinite loop to check in /proc/**/map
34: }
(dlv) regs
Rip = 0x0000000000499278
Rsp = 0x000000c000108eb0
Rax = 0x0000000000000000
Rbx = 0x000000c00011e000
Rcx = 0x0000000000000000
Rdx = 0x0000000000000000
Rsi = 0x0000000000000000
Rdi = 0x0000000000000000
Rbp = 0x000000c000108f78
R8 = 0x0000000000000000
R9 = 0x0000000000000000
R10 = 0xfffff80000000001
R11 = 0x0000000000000212
R12 = 0x000000c0000161b0
R13 = 0x000000000000003b
R14 = 0x0000000000000013
R15 = 0xffffffffffffffff
Rflags = 0x0000000000000202 [IF IOPL=0]
Es = 0x0000000000000000
Cs = 0x0000000000000033
Ss = 0x000000000000002b
Ds = 0x0000000000000000
Fs = 0x0000000000000000
Gs = 0x0000000000000000
Fs_base = 0x000000000054e5b0
Gs_base = 0x0000000000000000
$ cat /proc/70871/maps
00400000-0049a000 r-xp 00000000 08:01 4456778 /Workspace/go/src/musthavego/ch14/go_mem_layout_struct
0049a000-00538000 r--p 0009a000 08:01 4456778 /Workspace/go/src/musthavego/ch14/go_mem_layout_struct
00538000-0054e000 rw-p 00138000 08:01 4456778 /Workspace/go/src/musthavego/ch14/go_mem_layout_struct
0054e000-00581000 rw-p 00000000 00:00 0
c000000000-c004000000 rw-p 00000000 00:00 0
7fc392bef000-7fc394fa0000 rw-p 00000000 00:00 0
7fc394fa0000-7fc3a5120000 ---p 00000000 00:00 0
7fc3a5120000-7fc3a5121000 rw-p 00000000 00:00 0
7fc3a5121000-7fc3b6fd0000 ---p 00000000 00:00 0
7fc3b6fd0000-7fc3b6fd1000 rw-p 00000000 00:00 0
7fc3b6fd1000-7fc3b93a6000 ---p 00000000 00:00 0
7fc3b93a6000-7fc3b93a7000 rw-p 00000000 00:00 0
7fc3b93a7000-7fc3b9820000 ---p 00000000 00:00 0
7fc3b9820000-7fc3b9821000 rw-p 00000000 00:00 0
7fc3b9821000-7fc3b98a0000 ---p 00000000 00:00 0
7fc3b98a0000-7fc3b9900000 rw-p 00000000 00:00 0
7fff8d006000-7fff8d027000 rw-p 00000000 00:00 0 [stack]
7fff8d08f000-7fff8d093000 r--p 00000000 00:00 0 [vvar]
7fff8d093000-7fff8d095000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
$ objdump -h go_mem_layout_struct
go_mem_layout_struct: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0009828a 0000000000401000 0000000000401000 00001000 2**5
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rodata 00044167 000000000049a000 000000000049a000 0009a000 2**5
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .typelink 00000734 00000000004de300 00000000004de300 000de300 2**5
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .itablink 00000050 00000000004dea40 00000000004dea40 000dea40 2**5
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .gosymtab 00000000 00000000004dea90 00000000004dea90 000dea90 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .gopclntab 00058ab8 00000000004deaa0 00000000004deaa0 000deaa0 2**5
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .go.buildinfo 00000020 0000000000538000 0000000000538000 00138000 2**4
CONTENTS, ALLOC, LOAD, DATA
7 .noptrdata 0000e2c4 0000000000538020 0000000000538020 00138020 2**5
CONTENTS, ALLOC, LOAD, DATA
8 .data 00007790 0000000000546300 0000000000546300 00146300 2**5
CONTENTS, ALLOC, LOAD, DATA
9 .bss 0002d750 000000000054daa0 000000000054daa0 0014daa0 2**5
ALLOC
10 .noptrbss 00005310 000000000057b200 000000000057b200 0017b200 2**5
ALLOC
11 .zdebug_abbrev 000001e6 0000000000581000 0000000000581000 0014e000 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
12 .zdebug_line 00032ac5 0000000000581119 0000000000581119 0014e119 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
13 .zdebug_frame 00010144 000000000059daa3 000000000059daa3 0016aaa3 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
14 .debug_gdb_scripts 0000002d 00000000005a3643 00000000005a3643 00170643 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
15 .zdebug_info 0007d8a7 00000000005a3670 00000000005a3670 00170670 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
16 .zdebug_loc 00087fc6 00000000005d6e41 00000000005d6e41 001a3e41 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
17 .zdebug_ranges 00032a70 00000000005eeb07 00000000005eeb07 001bbb07 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
18 .note.go.buildid 00000064 0000000000400f9c 0000000000400f9c 00000f9c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
프로세스의 메모리 레이아웃 확인, 스택 포인터 레지스터의 값(Rsp), 각 변수의 주소를 확인해 본 결과 아래와 같은 사실을 알 수 있다.
- 현재 go 코드가 실행되고 있는 위치는 전통적인 스택 세그먼트가 아니다.
아마 go runtime이 스택 세그먼트 위에서 동작하며, 실제 go 코드를 수행하는 goroutine이 다른 세그먼트에서 동작하는 것으로 보인다.- procfs의 stack 세그먼트는 0x00007fff8d006000 ~ 0x00007fff8d027000
- 현재 스택 포인터 레지스터 값은 0x000000c000108eb0
- 변수들의 주소 값은 0x000000c000102060 ~ 0x000000c0001021e0
- 즉, 현재 goroutine이 사용중인 세그먼트는 0x000000c000000000 ~ 0x000000c004000000로 볼 수 있다.
- 변수들은 지역변수, 동적할당 변수 관계 없이 자체적인 힙 영역에 할당하는 것으로 보인다.
- 현재 스택 포인터 레지스터 값은 0x000000c000108eb0
- 변수들의 주소 값은 0x000000c000102060 ~ 0x000000c0001021e0
- 변수들의 주소값과 스택 포인터의 값이 약 0x6000 이상의 차이를 보이고 있다.
- 변수들이 지역변수, 동적할당 변수, 탈출분석에 의한 변수 관계없이 모두 같은 간격으로 배치되어있다. (0x60)
- 일반적으로 콜 스택이 큰 값에서 작은 값으로 자라고, 변수들이 선언 순서에 따라 주소 값이 커지는 것으로 보아
변수는 내부적으로 콜 스택에 저장된다고 보기 힘들 수 있다.
혹시 접근하는 변수 타입이 struct
라서 무조건 heap을 사용하는 것이고, 기본 자료형을 사용하면 stack에 값이 들어가는지 추가로 확인해봤다.
|
|
$ dlv exec ./go_mem_layout_int
Type 'help' for list of commands.
(dlv) b go_mem_layout_int.go:27
Breakpoint 1 (enabled) set at 0x499245 for main.main() ./go_mem_layout_int.go:27
(dlv) continue
pInt1: 0xc00001a0f0
pInt2: 0xc00001a0f8
pInt3: 0xc00001a100
pInt4: 0xc00001a108
pInt5: 0xc00001a110
> main.main() ./go_mem_layout_int.go:27 (hits goroutine(1):1 total:1) (PC: 0x499245)
Warning: debugging optimized function
24: fmt.Printf("pInt4: %p\n", pInt4)
25: fmt.Printf("pInt5: %p\n", pInt5)
26:
=> 27: for {
28: // Force infinite loop to check in /proc/**/map
29: }
(dlv) regs
Rip = 0x0000000000499245
Rsp = 0x000000c000108eb0
Rax = 0x0000000000000000
Rbx = 0x000000c00011e000
Rcx = 0x0000000000000000
Rdx = 0x0000000000000000
Rsi = 0x0000000000000000
Rdi = 0x0000000000000000
Rbp = 0x000000c000108f78
R8 = 0x0000000000000000
R9 = 0x0000000000000000
R10 = 0xfffff80000000001
R11 = 0x0000000000000212
R12 = 0x000000c0000161b0
R13 = 0x000000000000003b
R14 = 0x0000000000000013
R15 = 0xffffffffffffffff
Rflags = 0x0000000000000202 [IF IOPL=0]
Es = 0x0000000000000000
Cs = 0x0000000000000033
Ss = 0x000000000000002b
Ds = 0x0000000000000000
Fs = 0x0000000000000000
Gs = 0x0000000000000000
Fs_base = 0x000000000054e5b0
Gs_base = 0x0000000000000000
int
의 크기인 8 byte 간격으로 떨어진 것을 보아 기본 자료형이던, struct
정의 자료형이던 비슷한 증상이 나타나는 것을 확인할 수 있었다.
나와 같은 질문에 대해 Go언어 문서 FAQ에서는 변수가 스택에 저장되는지, 힙에 저장되는지 알 필요가 없다고 한다. 위 코드의 증상에 대한 설명을 추가하자면, 주소를 사용하는 변수는 힙에 할당될 가능성이 높다고 볼 수 있다.
Go의 메모리 할당에 대한 설명을 읽어보면, Go runtime에서는 전통적인 OS의 스택이 아닌, goroutine만의 콜 스택을 생성하고, 콜 스택이 overflow가 일어나기 전에 더 큰 콜 스택으로 복사하는 작업을 수행한다고 한다. 이 상황에서 지역변수를 포인터로 접근하게 되면, 콜 스택이 복사될 때 해당 포인터들이 댕글링 포인터가 될 수도 있기 때문에 해당 변수들을 힙에 할당할 후보가 되는 것 같다.
해당 글에서 알려준 빌드 옵션 -m -l
을 추가해서 컴파일러의 탈출 분석 과정을 확인하면, 모든 인스턴스를 탈출로 판단하여 heap에 넣는 것을 확인할 수 있다.
$ go build -gcflags '-m -l' go_mem_layout_struct.go
# command-line-arguments
./go_mem_layout_struct.go:38:6: moved to heap: d
./go_mem_layout_struct.go:11:6: moved to heap: data1
./go_mem_layout_struct.go:12:6: moved to heap: data2
./go_mem_layout_struct.go:12:23: new(Data) does not escape
./go_mem_layout_struct.go:16:24: new(Data) escapes to heap
./go_mem_layout_struct.go:23:15: new(Data) escapes to heap
./go_mem_layout_struct.go:26:12: ... argument does not escape
./go_mem_layout_struct.go:27:12: ... argument does not escape
./go_mem_layout_struct.go:28:12: ... argument does not escape
./go_mem_layout_struct.go:29:12: ... argument does not escape
./go_mem_layout_struct.go:30:12: ... argument does not escape
$ go build -gcflags '-m -l' go_mem_layout_int.go
# command-line-arguments
./go_mem_layout_int.go:33:6: moved to heap: d
./go_mem_layout_int.go:6:6: moved to heap: num1
./go_mem_layout_int.go:7:6: moved to heap: num2
./go_mem_layout_int.go:7:21: new(int) does not escape
./go_mem_layout_int.go:11:22: new(int) escapes to heap
./go_mem_layout_int.go:18:14: new(int) escapes to heap
./go_mem_layout_int.go:21:12: ... argument does not escape
./go_mem_layout_int.go:22:12: ... argument does not escape
./go_mem_layout_int.go:23:12: ... argument does not escape
./go_mem_layout_int.go:24:12: ... argument does not escape
./go_mem_layout_int.go:25:12: ... argument does not escape