벤치마크 함수가 있는 <file_name>_test.go
파일로 벤치마크를 할 수 있다. Sprint
와 Sprintf
중 어느 것이 더 성능이 좋고, 효율적으로 자원을 할당하는지 벤치마크 해볼 텐데, Sprint
가 문자열 포맷을 하는 동안 오버헤드가 없기 때문에 더 나을 것으로 보이지만 그렇지 않다. 추측보다는 실제 확인한 결과로 판단하자.
package main
import (
"fmt"
"testing"
)
var gs string
BenchmarkSprintBasic
은 Sprint
성능을 테스트한다. 벤치마크 하려는 모든 코드는 b.N
반복문 안에 있어야 한다. 벤치마크 도구가 처음 호출 할 때 b.N
은 1이고, 충분히 테스트가 진행될 때 까지 b.N
값은 지속적으로 증가한다. 전역변수 gs
에 fmt.Sprint
의 값을 반환하기 때문에 dead code 처럼 보이지 않게 된다.
func BenchmarkSprintBasic(b *testing.B) {
var s string
for i := 0; i < b.N; i++ {
s = fmt.Sprint("hello")
}
gs = s
}
BenchmarkSprintfBasic
은 Sprintf
의 성능을 테스트한다.
func BenchmarkSprintfBasic(b *testing.B) {
var s string
for i := 0; i < b.N; i++ {
s = fmt.Sprintf("hello")
}
gs = s
}
go test -run none -bench . -benchtime 3s -benchmem
goos: darwin
goarch: amd64
BenchmarkSprintBasic-16 52997451 56.9 ns/op 5 B/op 1 allocs/op
BenchmarkSprintfBasic-16 72737234 45.7 ns/op 5 B/op 1 allocs/op
PASS
ok command-line-arguments 6.637s
하위 테스트를 하듯이, 하위 벤치마크도 가능하다.
package main
import (
"fmt"
"testing"
)
BenchmarkSprintSub
는 Sprint
와 관련한 모든 하위 벤치마크를 한다.
func BenchmarkSprintSub(b *testing.B) {
b.Run("none", benchSprint)
b.Run("format", benchSprintf)
}
benchSprint
는 Sprint
의 성능을 테스트한다.
func benchSprint(b *testing.B) {
var s string
for i := 0; i < b.N; i++ {
s = fmt.Sprint("hello")
}
gs = s
}
benchSprintf
는 Sprintf
의 성능을 테스트한다.
func benchSprintf(b *testing.B) {
var s string
for i := 0; i < b.N; i++ {
s = fmt.Sprintf("hello")
}
gs = s
}
go test -run none -bench . -benchtime 3s -benchmem
goos: darwin
goarch: amd64
BenchmarkSprintSub/none-16 54088082 60.6 ns/op 5 B/op 1 allocs/op
BenchmarkSprintSub/format-16 67906119 52.3 ns/op 5 B/op 1 allocs/op
PASS
ok command-line-arguments 7.131s
하위 벤치마크를 할 수 있는 다른 방법들:
go test -run none -bench BenchmarkSprintSub/none -benchtime 3s -benchmem
go test -run none -bench BenchmarkSprintSub/format -benchtime 3s -benchmem
스택 트레이스 리뷰(Review Stack Trace)
어떻게 스택 트레이스를 할 수 있을까?
package main
func main() {
이 예제는 배열, 문자열, 정수를 사용한다. 길이 2, 용량 4의 배열을 만들고 그 배열 값을 예제 함수에 전달한다.
example(make([]string, 2, 4), "hello", 10)
}
예제는 내장 함수 panic
을 호출하여 스택 트레이스을 보여준다.
func example(slice []string, str string, i int) {
panic("Want stack trace")
}
예제를 통해 출력되는 결과:
panic: Want stack trace
goroutine 1 [running]:
main.example(0xc420053f38, 0x2, 0x4, 0x1066c02, 0x5, 0xa)
/Users/hoanhan/go/src/github.com/hoanhan101/ultimate-go/go/profiling/stack_trace.go:18 +0x39
main.main()
/Users/hoanhan/go/src/github.com/hoanhan101/ultimate-go/go/profiling/stack_trace.go:13 +0x72
exit status 2
컴파일러는 어떤 라인에서 문제가 생겼는지 알려준다. 스택 트레이스를 사용했을 때 더 좋은 점은, 함수에 전달되는 값을 정확히 알 수 있다. 스택 트레이스는 데이터 구조를 펼쳐서 표시한다. slice
는 3개의 word로 표시 되는데, 첫 번째 word는 포인터, 두 번째 word는 2(길이), 세 번째 word는 4(용량)이다. string
은 2개의 word로 표시 되는데, "hello"
는 길이가 5인 5byte의 문자열이고, 첫 번째 word는 포인터, 두 번째 word는 5(길이)이다. 마지막으로 남은 word는 10(정수)이다. 스택 트레이스에서 볼 수 있는 main.example (0xc420053f38, 0x2, 0x4, 0x1066c02, 0x5, 0xa)
에서 각각에 해당되는 값은 포인터 주소, 2, 4, 포인터 주소, 5, a(16진수로 표기된 10)
이다.
스택 트레이스를 사용하는 것 만으로도 사용된 값을 확인하거나 필요한 데이터를 얻을 수 있다. Dave의 error 패키지를 사용하고, 몇몇 컨텍스트를 더하거나 log 패키지를 추가하면 디버깅 할 때 문제에 대한 더 많은 정보를 얻을 수 있다.
Packing
값을 채우는 스택 트레이스의 예.
example
함수에 할당되는 값들은 전부 1byte이다.
package main
func main() {
example(true, false, true, 25)
}
func example(b1, b2, b3 bool, i uint8) {
panic("Want stack trace")
}
출력 결과:
panic: Want stack trace
goroutine 1 [running]:
main.example(0xc419010001)
/Users/hoanhan/go/src/github.com/hoanhan101/ultimate-go/go/profiling/stack_trace_2.go:12 +0x39
main.main()
/Users/hoanhan/go/src/github.com/hoanhan101/ultimate-go/go/profiling/stack_trace_2.go:8 +0x29
exit status 2
이번에는 스택 트레이스가 1 word 밖에 보여주지 않는데, 이 4바이트는 32비트 환경에선 half-wold로, 64비트 환경에선 full-word로 표시된다. 예제에 사용된 시스템은 리틀 엔디언을 사용하고 있기 때문에 오른쪽에서 왼쪽으로 읽어야 한다. 0xc419010001
을 아래와 같이 나타낼 수 있다:
Bits | Binary | Hex | Value |
---|---|---|---|
00-07 | 0000 0001 | 01 | true |
08-15 | 0000 0000 | 00 | false |
16-23 | 0000 0001 | 01 | true |
24-31 | 0001 1001 | 19 | 25 |
메모리 트레이싱
메모리 트레이싱은 코드가 실행될 때 GC 및 힙 메모리에 관련해서 잘 작동하는지 분석을 제공한다. 다음은 메모리 릭을 일으키는 예제이다.
package main
import (
"os"
"os/signal"
)
func main() {
아래 코드는 고루틴을 생성할 때 메모리 릭을 일으킨다. 코드가 종료될 때 까지 계속 Key-value를 할당한다.
go func() {
m := make(map[int]int)
for i := 0; ; i++ {
m[i] = i
}
}()
Ctrl-C 로 코드를 종료한다.
sig := make(chan os.Signal, 1)
signal.Notify(sig)
<-sig
}
GODEBUG
라는 환경변수를 사용하게 되면 메모리와 스케줄러를 확인할 수 있게 된다. 빌드 및 실행 방법:
빌드 : go build memory_tracing.go
실행 : GODEBUG=gctrace=1 ./memory_tracing
GODEBUG=gctrace=1
을 설정하면 가비지 컬렉터는 각 수집마다 수집된 메모리와 실행 시간을 요약하여 한줄씩 출력한다.
문제가 발생한 부분을 찾는 방법:
gc {0} @{1}s {2}%: {3}+...+{4} ms clock, {5}+...+{6} ms cpu, {7}->{8}->{9} MB, {10} MB goal, {11} P
의미: {0} : gc 실행 횟수 {1} : 프로그램이 실행 된 시간. {2} : gc가 차지하는 CPU의 비율. {3} : 프로그램 실행 시간 - 프로그램의 지연시간이나 리소스를 사용할 수 있을 때까지 대기하는 시간을 포함한 실시간 측정 값. {4} : 프로그램 실행 시간. 보기보다 중요한 숫자. {5} : CPU 클럭 {6} : CPU 클럭 {7} : gc가 시작되기 전의 힙 크기. {8} : gc 실행 후 힙 크기. {9} : 라이브 힙의 크기. {10} : gc의 목표, 페이싱 알고리즘. {11} : 프로세스 수.
실행 결과:
gc 1 @0.007s 0%: 0.010+0.13+0.030 ms clock, 0.080+0/0.058/0.15+0.24 ms cpu, 5->5->3 MB, 6 MB goal, 8 P
gc 2 @0.013s 0%: 0.003+0.21+0.034 ms clock, 0.031+0/0.030/0.22+0.27 ms cpu, 9->9->7 MB, 10 MB goal, 8 P
gc 3 @0.029s 0%: 0.003+0.23+0.030 ms clock, 0.029+0.050/0.016/0.25+0.24 ms cpu, 18->18->15 MB, 19 MB goal, 8 P
gc 4 @0.062s 0%: 0.003+0.40+0.040 ms clock, 0.030+0/0.28/0.11+0.32 ms cpu, 36->36->30 MB, 37 MB goal, 8 P
gc 5 @0.135s 0%: 0.003+0.63+0.045 ms clock, 0.027+0/0.026/0.64+0.36 ms cpu, 72->72->60 MB, 73 MB goal, 8 P
gc 6 @0.302s 0%: 0.003+0.98+0.043 ms clock, 0.031+0.078/0.016/0.88+0.34 ms cpu, 65->66->42 MB, 120 MB goal, 8 P
gc 7 @0.317s 0%: 0.003+1.2+0.080 ms clock, 0.026+0/1.1/0.13+0.64 ms cpu, 120->121->120 MB, 121 MB goal, 8 P
gc 8 @0.685s 0%: 0.004+1.6+0.041 ms clock, 0.032+0/1.5/0.72+0.33 ms cpu, 288->288->241 MB, 289 MB goal, 8 P
gc 9 @1.424s 0%: 0.004+4.0+0.081 ms clock, 0.033+0.027/3.8/0.53+0.65 ms cpu, 577->577->482 MB, 578 MB goal, 8 P
gc 10 @2.592s 0%: 0.003+11+0.045 ms clock, 0.031+0/5.9/5.2+0.36 ms cpu, 499->499->317 MB, 964 MB goal, 8 P
처음에는 빠른데, 점점 느려지기 시작한다. GC가 실행될 때 마다 힙 사이즈가 증가하는 부분에서 메모리 릭이 발생함을 알 수 있다.
당신이 The Ultimate Go Book을 잘 이해할 수 있었는지 이메일(hoanhan101@gmail.com)로 알려주시길 바랍니다.
만약, 당신이 나의 활동 및 프로젝트, 그리고 더 나은 소프트웨어 엔지니어가 되는데 관심이 있다면, 내 웹사이트 https://hoanhan101.github.io/ 에 자유롭게 방문해주시길 바랍니다.
읽어주셔서 고맙습니다. 행운을 빕니다!