쿼카러버의 기술 블로그
[golang] benchmark 하는 법 간단 이해 (go에서 벤치마크 하는법) 본문
앞 글에서 profiling에 대해 알아보며 간단하게 benchmark와의 차이에 대해 다루었다. 사실 성능 개선을 하거나 리팩토링을 하기 위해서는 프로파일링 뿐 아니라, benchmark도 매우 중요하다. 이 알고리즘을 사용하면 진짜 성능이 좋아질지, 자주 호출되는 로직을 수정했는데 성능적으로 괜찮을지 등의 증거를 확보할 수 있기 때문이다. 벤치마크를 해본 적이 없다면 이 기회에 배워서 그냥 추측만으로 할게 아니라 실제로 측정해보자.
예상했겠지만 본 글은 간단하게 벤치마크가 무엇인지 그리고 go에서 benchmark
하는 방법에 대해 간단하게 살펴보려고 한다.
벤치마크란?
benchmark
란 특정 코드 segment를 여러번 반복 실행하고 해당 코드의 성능을 측정하는 함수를 의미한다.
스포일러로 아래 코드를 보자. b.N
횟수만큼 코드를 실행해보고, 실행하는데 얼마나 걸렸는지 등을 확인해볼 수 있다.
// from search_test.go
func BenchmarkSearch(b *testing.B) {
// run the Search function b.N times
for n := 0; n < b.N; n++ {
Search("target word)
}
}
go에서는 built-in tool로 testing
패키지와 go
tool을 사용해서 다른 dependency를 설치할 필요 없이 바로 benchmark
를 실행해볼 수 있다.
go에서 benchmark하는 법
Writing a benchmark
go에서 벤치마크를 수행하기 위해서는 아래 2가지를 기억해야 한다.
(1) 벤치마크를 수행하는 함수의 이름은 Benchmark
로 시작하는 mixedCap로 작성한다.
- 예 : BenchmarkSearch, BenchmarkFibo, BenchmarkGet
(2) 벤치마크를 수행하는 함수를 정의할 때 *testing.B
타입의 parameter가 있어야한다.
(3) 벤치마크를 수행하는 함수를 *_test.go
파일에 저장한다.
예를 들어보자. 아래의 함수에 대해 Benchmark를 수행하고 싶으면
func ContainsStr(source, target string) bool {
return strings.Contains(source, target)
}
아래와 같이 Benchmark
함수를 정의하면 된다.
func BenchmarkStrContains(b *testing.B) {
for i := 0; i < b.N; i++ {
ContainsStr(STR, "richet")
}
}
contains_bytes_test.go
파일을 다음과 같이 생성하고 실행했다고 가정해보자.
package main
import (
"strings"
"testing"
)
const STR = `my name is richet`
func ContainsStr(source, target string) bool {
return strings.Contains(source, target)
}
func BenchmarkStrContains(b *testing.B) {
for i := 0; i < b.N; i++ {
ContainsStr(STR, "richet")
}
}
위 파일이 저장된 경로에서 벤치마크를 수행하려면 아래 명령어를 실행하면 된다.
go test -bench=. --benchtime=1000000x
-bench
flag : regular expression을 입력하는 부분으로.
은 모든 벤치마크를 수행한다는 것을 의미한다.- 간단한 예로
go test -bench=Strr
입력하면 매칭되는게 없어서 실행이 안됨 - 이를 잘 활용하면 정의된 벤치마크 중, 일부만 수행하도록 할 수도 있다.
- 간단한 예로
-benchtime
flag :- 각 벤치마크는 최소 1초동안 수행하도록 설정돼있다.
Nx
를 뒤에 붙이면N
번으로 실행 횟수를 지정할 수 있고20s
와 같이s
를 붙이면 20초동안 실행하도록 할 수도 있다.
위와 같이 실행하면 출력결과는 아래와 같다
goos: darwin
goarch: amd64
pkg: github.com/getveryrichet/goplayground/benchmark/example
cpu: Intel(R) Core(TM) i7-8700B CPU @ 3.20GHz
BenchmarkByteContains-12 1000000 7.262 ns/op
PASS
ok github.com/getveryrichet/goplayground/benchmark/example 0.833s
1000000
번 실행했고,7.262 ns/op
평균 실행 시간 (7.562 nano second가 걸림)goos
,goarch
,pkg
,cpu
는 운영체제 관련된 정보들임.-12
suffix는 사용된 CPU 코어 갯수를 의미
unit test들이 같이 정의돼있는 경우
unit test들이 같은 테스트 파일에 정의돼있는데, 벤치마크만 실행하고 싶은 경우에는 아래와 같이 -run flag를 같이 넣어주면 된다
$ go test -bench=. -run=^#
여러 input을 넣고 벤치마크 하는 법
벤치마크를 수행하다보면, 함수의 input이 어떻냐에 따라 어떻게 반응하는지 측정해보고 싶은 케이스가 있기 마련이다. 이런 경우에는 위에서 수행했던 방식과는 다르게 table driven test
패턴과 b.Run()
메소드를 활용해 사전에 정의해둔 input들에 대한 sub-benchmark를 수행할 수 있다.
아래는 그 예시다.
var table = []struct {
input string
}{
{input: "hah"},
{input: "jane"},
{input: "df"},
{input: "foo"},
}
func BenchmarkMultipleStrContains(b *testing.B) {
for _, v := range table {
b.Run(fmt.Sprintf("searching %s", v.input), func(b *testing.B) {
for i := 0; i < b.N; i++ {
ContainsStr(STR, v.input)
}
})
}
}
위와 같이 정의해두고, 실행하면 아래와 같은 결과를 볼 수 있다
goos: darwin
goarch: amd64
pkg: github.com/getveryrichet/goplayground/benchmark/example
cpu: Intel(R) Core(TM) i7-8700B CPU @ 3.20GHz
BenchmarkStrContains-12 159015663 7.771 ns/op
BenchmarkMultipleStrContains/searching_hah-12 163173175 7.367 ns/op
BenchmarkMultipleStrContains/searching_jane-12 165956943 7.366 ns/op
BenchmarkMultipleStrContains/searching_df-12 166833112 7.188 ns/op
BenchmarkMultipleStrContains/searching_foo-12 131202492 7.719 ns/op
위 예시는 이해를 돕기 위한 것이지, 실제로 ContainStr을 테스트하는데 위와 같은 input을 주진 않는다. 그냥 이렇게 정의해서 활용할 수 있다 정도로 이해하고 사용하자.
memory allocation 통계치 확인하는 법
go runtime은 테스트하고 있는 코드들의 memory allocation을 트랙킹하고 있기 때문에 수행시간 뿐 아니라 memory allocation도 개선하는데 활용할 수 있다.
memory allocation 통계치를 확인하기 위해서는 -benchmem
flag만 붙여주면 된다.
go test -bench=. -benchmem
goos: darwin
goarch: amd64
pkg: github.com/getveryrichet/goplayground/benchmark/example
cpu: Intel(R) Core(TM) i7-8700B CPU @ 3.20GHz
BenchmarkStrContains-12 146840930 7.895 ns/op 0 B/op 0 allocs/op
BenchmarkMultipleStrContains/searching_hah-12 169447662 7.502 ns/op 0 B/op 0 allocs/op
BenchmarkMultipleStrContains/searching_jane-12 160095481 8.187 ns/op 0 B/op 0 allocs/op
BenchmarkMultipleStrContains/searching_df-12 149567868 7.542 ns/op 0 B/op 0 allocs/op
BenchmarkMultipleStrContains/searching_foo-12 151275692 7.755 ns/op 0 B/op 0 allocs/op
- 결과물에서 4번째 column은 각 operation당 allocate된 byte수를 의미하고
- 4번째 column은 각 operation당 number of allocations을 의미한다.
benchstat을 활용해 벤치마크 결과 비교하기
아래 커맨드로 benchstat을 설치하고
$ go install golang.org/x/perf/cmd/benchstat@latest
두 개의 benchmark 결과를 출력했다고 가정해보자
go test -bench=. | tee old.txt
go test -bench=. | tee new.txt
tee
stdin을 읽고 stdout과 하나 이상의 파일에 write할 때 쓰는 명령어임
아래 커맨드를 실행해보면
benchstat old.txt new.txt
name old time/op new time/op delta
StrContains-12 8.09ns ± 0% 7.69ns ± 0% ~ (p=1.000 n=1+1)
MultipleStrContains/searching_hah-12 7.25ns ± 0% 6.99ns ± 0% ~ (p=1.000 n=1+1)
MultipleStrContains/searching_jane-12 7.47ns ± 0% 7.04ns ± 0% ~ (p=1.000 n=1+1)
MultipleStrContains/searching_df-12 7.67ns ± 0% 7.22ns ± 0% ~ (p=1.000 n=1+1)
MultipleStrContains/searching_foo-12 7.24ns ± 0% 7.09ns ± 0% ~ (p=1.000 n=1+1)
delta
는 성능 변화의 percentage change값을 의미한다.- 자세한 내용은 https://pkg.go.dev/golang.org/x/perf/cmd/benchstat 를 참고하자.
물론 간단한 설명을 위해 복잡성을 줄이려고 동일한 테스트의 결과물을 비교해서 거의 차이가 없지만 위와 같이 활용해볼 수 있겠다.
자 이렇게 benchmark가 무엇인지에 대해 간단히 알아보았다. 그 외에도 StartTimer, StopTimer와 같은 재밌는 메소드들도 있기 때문에, 관심있는 분들은 더 공부해보자.
참고 자료
https://gobyexample.com/testing-and-benchmarking
https://pkg.go.dev/golang.org/x/perf/cmd/benchstat
https://blog.logrocket.com/benchmarking-golang-improve-function-performance/
https://medium.com/nerd-for-tech/benchmarking-your-solution-in-go-940b528416c