출처: https://qiita.com/laqiiz/items/caa82d84c8d3ac07cfe0 

이 문서에서는 Go 벤치 마크 기능에 대한 것이다.

평소 나는 회사용 애플리케이션의 서버 사이드 개발에 Go를 이용하고 있다. 지금까지의 경험에서 성능 병목 현상은 Go로 작성된 애플리케이션 로직 부분이 아닌 외부 데이터 저장소 (RDB나 AWS DynamoDB) 질의 부분인 것이 많았지만, 몇번 인가 매우 느린 코드를 썼던 적이 있다. 이런 경우에도 대응할 수 있도록 코드의 어느 부분이 처리 부하가 높은 것인지 새기는 마음으로 Go의 벤치 마크 기능을 조사한 결과를 남긴다.

Go Benchmark 개요

Go는 testing 이라는 패키지와 유틸리티 도구(CPU 등의 프로파일링 등)가 표준 기능으로 제공 되고 있다. 기본형 테스트는 func TestXxx(*testing.T) 라는 형식으로 테스트 코드를 작성하고, go test 명령을 실행할 수 있다.

벤치 마크 테스트도 마찬가지로 func BenchmarkXxx(*testing.B) 형식으로 테스트 코드를 작성하고, go test -bench=. 로 실행할 수 있다.

요약은 공식 문서 Package testing 에 쓰여 있다. -bench 시 사용할 수 있는 옵션은 Testing flags 에 정리 되어 있다.

옵션을 보면 많은 기능이 있는 것을 알 수 있지만, 이번 벤치 마크 결과 시각화 부분을 중심으로 살펴 보겠다.

Benchmark 코드 준비

우선 무엇이든 좋은 벤치마킹 대상으로 하는 함수를 제공한다.

벤치마킹 대상의 코드(어떤 처리도 좋지만, 이번은 이미지 크기를 조정하는 처리를 했다)

import (
   
"github.com/nfnt/resize"
   
"image/jpeg"
   
"os"
)

func ResizeJPEG(src, dest string, quality int) error {
   f, err := os.Open(src)
   
if err != nil {
       
return err
   }

   img, err := jpeg.Decode(f)
   
if err != nil {
       
return err
   }
   f.Close()

   out, err := os.Create(dest)
   
if err != nil {
       
return err
   }
   
defer out.Close()

   m := resize.Resize(
1000, 0, img, resize.Lanczos3)
   
return jpeg.Encode(out, m, &jpeg.Options{
       Quality: quality,
   })
}

이에 대응하는 벤치 마크 코드를 준비한다. 비교를 위해 JPEG의 Quality를 75, 100의 2종류를 준비했다.

import "testing"

func BenchmarkResizeJPEG_q75(t *testing.B) {
   err := ResizeJPEG(
"src.jpg", "resize_q75.jpg", 75)
   
if err != nil {
       t.Fatal(
"resize src.jpg: ", err)
   }
}

func BenchmarkResizeJPEG_q100(t *testing.B) {
   err := ResizeJPEG(
"src.jpg", "resize_q100.jpg", 100)
   
if err != nil {
       t.Fatal(
"resize src.jpg: ", err)
   }
}

이 코드에 go test -bench=. 로 실행한다.

준비한 벤치 마크 함수 당, 실행 횟수 · 1회당 실행에 걸린 시간(ns/op) 를 얻을 수 있다.

벤치 마크 결과의 시각화

go test -bench=. 는 아주 실행이 간단하고 이것만으로도 충분할 수도 있다고 생각한다. 한편 벤치마킹 대상 함수 자체를 튜닝하고 싶을 때 병목 현상이 함수의 어디에 있는지 알기는 어렵다.

따라서 순서대로 처리를 추적해보자. 추적 방법은 먼저 -trace 옵션으로 실행 추적을 출력하고,  go tool 로 시각화한다.

$ go test  -bench =.  -trace a.trace

이제 a.trace 라는 파일이 출력된다. 만약 출력 되지 않으면 벤치 마크 테스트가 실패 했을 가능성이 있다. a.trace는 고루틴 만들기 / 차단 / 차단 해제, 시스템 콜의 시작 / 종료 / 블럭, GC 관련 이벤트, 힙 크기 변경, 프로세서의 시작 / 정지 등의 정보를 바이너리 형식으로 작성되어 있다. 자세한 내용은 Package trace 를 참고하기 바란다.

이 a.trace 파일을 인수로 하여 go tool trace 를 실행한다. --http 로 시작하면 서버의 포트를 지정해야한다.

그러면 아래와 같은 페이지가 브라우저에 표시된다.

여기에서 top 1의 View trace를 보면 아래와 같은 그래프를 볼 수 있다.

내용은 가로 축은 시간으로, GC와 코어 당 이용률을 알 수 있다. 상자를 드래그 하면 확대 · 축소 할 수 있기 때문에 신경 쓰이는 부분을 핀 포인트로 확대 할 수 있으므로 병목 현상이 있을 것 같은 곳은 여기에서 확인한다.

Zoom In / Out 으로 벤치마킹 대상의 함수가 호출하는 resize 패키지의 실행 시간 등을 알 수 있다. 이번에는 자신이 작성한 코드가 거의 없는데, 이 밖에도 여러 패키지 호출이 같은 함수를 벤치마킹 했을 때는 처리 시간의 내역을 알 수 있을 것이다.

CPU 프로파일링

이어 같은 벤치 마크 코드에 대해서 CPU 프로파일링을 한다. CPU 프로파일링은 비교적 런타임 부하가 작은 처리에서 실행 중인 gorutine 호출 추적을 수집하여 분석한다.

trace의 경우와 마찬가지로 -cpuprofile 옵션으로 마찬가지로 .prof 파일이 출력되므로 우선 벤치 마크 테스트를 수행한다.

출력된 a.prof 에 대해 go tool pprof 로 실행하여 시각화한다.

※ 시각화에는 Graphviz가 필요하다. 설치되어 있지 않다면 여기를 보고 환경 구축을 한다.

Graphviz가 설치된 단말기의 브라우저에서 http://localhost:6060/ui/ 에서 확인하면 아래와 같은 호출 그래프가 출력된 것을 볼 수 있다.

처리가 무거울 것 같은 부분은 강조 표시 되어 있는 것으로 알기 쉽다. 그래프를 보면 resize 처리에 숨어 있지만 jpeg의 Decode 나 jpeg의 Encode 부분도 처리가 무거운 것을 알 수 있다.

View 탭에서 "Frame Graph"를 선택하면 아래와 같은 프레임 그래프도 볼 수 있다.

사람에 따라서는 이쪽이 이해하기 쉬울 수도 있다

이 그래프에 의해 만약 이 벤치마킹 대상의 함수를 조정하려고 할 때는 resize 처리뿐만 아니라, Decode이나 Encode를 다른 처리로 비동기적으로 짤라내는 선택이나 잘 배치 방식으로 처리 할 수 없을까라는 방법을 고려하는 판단의 자료도 된다.

정리

Go 벤치 마크라고 하면, 함수의 마이크로 벤치 마크를 위한 도구로 함수 내부 프로파일링을 하는 기능은 없다고 생각했지만 기존 Tool 세트로 시각화 분석도 가능하다.