고 고급 #6 프로파일링 — pprof와 benchmark
#5 unsafe와 cgo 다음, 이번엔 정반대 결의 도구. 측정.
“Don’t guess; measure.”
성능 문제는 거의 항상 — 우리가 의심한 곳이 아닌 다른 곳에서 발생합니다. Go는 표준 도구가 강력해서 — 짐작 대신 빠르게 측정으로 갈 수 있습니다.
benchmark — 표준 도구 #
// adder_test.go
package adder
import "testing"
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}go test -bench=. -benchmemBenchmarkAdd-8 1000000000 0.30 ns/op 0 B/op 0 allocs/opb.N— 측정 시간이 안정적으로 나올 만큼 자동 조정-benchmem— 할당 정보 추가ns/op— 한 번 실행에 걸리는 시간B/op/allocs/op— 메모리 할당량과 횟수
benchmark 작성의 기본기 #
func BenchmarkParse(b *testing.B) {
data := loadBigInput() // 무거운 준비
b.ResetTimer() // 여기서부터 측정
for i := 0; i < b.N; i++ {
Parse(data)
}
}준비 비용은 — ResetTimer로 분리. b.StopTimer / b.StartTimer로 루프 안에서도 일시 정지 가능.
컴파일러 최적화 회피 #
func BenchmarkSum(b *testing.B) {
for i := 0; i < b.N; i++ {
sum(1, 2) // ✗ 결과 안 쓰면 컴파일러가 통째로 제거할 수 있음
}
}해결 — 결과를 패키지 레벨 변수에 대입(컴파일러가 제거 못 하게).
var benchResult int
func BenchmarkSum(b *testing.B) {
var r int
for i := 0; i < b.N; i++ {
r = sum(1, 2)
}
benchResult = r
}benchstat — 두 결과 비교 #
go test -bench=. -count=10 > before.txt
# 코드 수정
go test -bench=. -count=10 > after.txt
go install golang.org/x/perf/cmd/benchstat@latest
benchstat before.txt after.txt │ before │ after │
│ sec/op │ sec/op vs base │
Parse-8 │ 1.23µ ± 2% │ 0.85µ ± 1% -30.89% (p=0.000 n=10)p 값으로 유의미한 차이인지 판단. 한 번만 돌리면 노이즈에 속습니다 — -count=10 권장.
CPU 프로파일 #
go test -bench=. -cpuprofile=cpu.out
go tool pprof cpu.out(pprof) top
Showing nodes accounting for 1.23s, 87.86% of 1.4s total
flat flat% sum% cum cum%
0.43s 30.71% 30.71% 0.65s 46.43% parse
0.31s 22.14% 52.86% 0.31s 22.14% hash
...
(pprof) list parse
(pprof) web ← 브라우저에서 콜그래프top으로 가장 시간을 많이 쓰는 함수 보고, list <fn>으로 라인별 시간 확인.
메모리 프로파일 #
go test -bench=. -memprofile=mem.out
go tool pprof -alloc_space mem.out두 가지 시점:
-alloc_space— 누적 할당량 (이게 GC 부하를 가장 잘 보여줌)-inuse_space— 현재 살아 있는 메모리
핫 패스에서 할당이 많으면 — GC가 자주 돌아 throughput이 낮아집니다. 보통 escape analysis와 함께 분석.
Escape analysis #
go build -gcflags='-m' main.go./main.go:5:9: &User{...} escapes to heap스택에 남아도 됐을 객체가 — 어떤 이유로 힙 할당으로 빠졌는지 알려 줍니다. 메모리 할당을 줄이는 작업의 출발점으로 삼기 좋습니다.
프로덕션 프로파일 — net/http/pprof #
import (
"net/http"
_ "net/http/pprof" // /debug/pprof/* endpoint 자동 등록
)
func main() {
go http.ListenAndServe(":6060", nil)
// 본 서버는 별도로...
}# 30초간 CPU 프로파일
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# 현재 메모리
go tool pprof http://localhost:6060/debug/pprof/heap
# 현재 고루틴
go tool pprof http://localhost:6060/debug/pprof/goroutine프로덕션에서 — 서비스를 멈추지 않고 프로파일을 뜰 수 있습니다. 단, 6060 포트는 외부에 노출되지 않게 주의해야 합니다(internal only).
고루틴 누수 찾기 #
curl http://localhost:6060/debug/pprof/goroutine?debug=1현재 살아 있는 모든 고루틴의 스택. 수가 점점 늘면 누수. 중급 #3에서 본 누수 패턴이 의심될 때 첫 번째 도구.
Trace — 시간축 분석 #
CPU/메모리 프로파일이 “어디” 라면, trace는 “언제”.
go test -bench=. -trace=trace.out
go tool trace trace.out브라우저가 열리고 — 고루틴 스케줄링, GC 시점, 시스템 콜 같은 게 시간축으로 보입니다. GC가 자주 도는지, 고루틴이 starvation 되는지 같은 시간 축 문제에 어울립니다.
Race detector #
#2에서 잠깐 봤습니다.
go test -race ./...
go run -race main.go테스트와 로컬 실행에서는 항상 켜두는 게 좋습니다. CI에서도 race build를 별도로 돌리는 게 좋습니다. 오버헤드가 5~10배 정도 커서 — 프로덕션에서는 안 켭니다.
측정 워크플로우 #
전형적인 흐름:
- 사용자/메트릭이 느리다고 알려 줌
- CPU 프로파일 → 어느 함수에 시간이 쓰이는지
- (메모리 의심이면) 메모리 프로파일 → 어디서 많이 할당
- 의심 부분에 벤치마크 작성 — 재현 가능한 측정 환경
- 수정 + benchstat으로 비교 — 정말 빨라졌는지
- 본 환경에 다시 배포 — 메트릭으로 재확인
핵심은 — 매 단계마다 측정으로 검증. 추측만으로 최적화하면 — 실제로 더 느려지는 경우가 흔합니다.
자주 만나는 경우 #
문자열 연결 #
var s string
for _, p := range parts {
s += p // ✗ 매번 새 string 할당
}해결 — strings.Builder.
var b strings.Builder
for _, p := range parts {
b.WriteString(p)
}
s := b.String()slice의 capacity 미지정 #
result := make([]int, 0, len(items)) // ✓ capacity 미리
for _, x := range items {
result = append(result, transform(x))
}make([]int, 0)으로 시작하면 — 여러 번 재할당. 미리 capacity를 주면 1 회 할당으로 끝.
map의 capacity #
m := make(map[string]int, expectedSize)map도 — 예상 크기를 주면 reallocation 줄일 수 있습니다.
인터페이스 박싱 #
var any interface{} = 42 // int → interface{} 박싱 (heap 할당 가능)핫 루프에서 interface{} (또는 any)로 변환하는 일이 잦으면 할당이 누적. 가능하면 — 구체 타입으로.
마무리 #
이번 글에서 정리한 내용:
- benchmark —
b.N,-benchmem,ResetTimer, 결과 보존으로 최적화 회피 - benchstat — 두 측정의 통계적 비교
- CPU 프로파일 —
top,list,web - 메모리 프로파일 —
-alloc_space가 보통 더 유용 - escape analysis —
-gcflags='-m'로 힙 할당 원인 - net/http/pprof — 프로덕션 실시간 측정
- trace — 시간축, GC, 스케줄링
- race detector — 항상 테스트에서 켜기
- 워크플로우: 메트릭 → 프로파일 → 벤치 → 수정 → benchstat → 재확인
다음 글(#7 코드 생성)에서는 — Go가 자주 권장하는 또 다른 길. reflect의 비용을 피하면서 자동화하는 방법, go generate와 stringer 같은 표준 도구를 정리합니다.