Google Benchmark로 C++ 코드 벤치마킹하기

Everett Pompeii

Everett Pompeii


벤치마킹이란 무엇인가요?

벤치마킹은 코드의 성능을 테스트하여 코드가 얼마나 빠르게(지연시간) 또는 얼마나 많은 양(처리량)의 작업을 수행할 수 있는지를 확인하는 실습입니다. 이는 소프트웨어 개발에서 종종 간과되지만, 빠르고 성능 좋은 코드를 작성하고 유지하기 위해 매우 중요한 단계입니다. 벤치마킹은 개발자가 다양한 작업 부하와 조건에서 코드가 얼마나 잘 수행되는지를 이해하는 데 필요한 메트릭을 제공합니다. 기능 회귀를 방지하기 위해 단위 테스트와 통합 테스트를 작성하는 것과 같은 이유로, 성능 회귀를 방지하기 위해 벤치마크를 작성해야 합니다. 성능 버그도 버그입니다!

C++로 FizzBuzz 작성하기

벤치마크를 작성하기 위해서는 벤치마크할 소스 코드가 필요합니다. 처음으로 매우 간단한 프로그램인, FizzBuzz를 작성해 봅시다.

FizzBuzz의 규칙은 다음과 같습니다:

1부터 100까지의 정수를 출력하는 프로그램을 작성하십시오:

  • 3의 배수인 경우 Fizz를 출력합니다
  • 5의 배수인 경우 Buzz를 출력합니다
  • 3과 5의 공배수인 경우 FizzBuzz를 출력합니다
  • 그 외의 경우에는 숫자를 출력합니다.

FizzBuzz를 작성하는 방법은 여러 가지가 있습니다. 그래서 저는 제가 가장 좋아하는 방법을 선택하겠습니다:

#include <iostream>
int main()
{
for (int i = 1; i <= 100; i++)
{
if ((i % 15) == 0)
std::cout << "FizzBuzz\n";
else if ((i % 3) == 0)
std::cout << "Fizz\n";
else if ((i % 5) == 0)
std::cout << "Buzz\n";
else
std::cout << i << "\n";
}
return 0;
}
  • 1에서 100까지 반복하고, 매 반복 후에 증가합니다.
  • 각 숫자에 대해 모듈러스(나머지)를 계산합니다.
  • 나머지가 0이면, 해당 숫자는 주어진 인수의 배수입니다:
    • 15로 나누었을 때 나머지가 0이면 FizzBuzz를 출력합니다.
    • 3으로 나누었을 때 나머지가 0이면 Fizz를 출력합니다.
    • 5로 나누었을 때 나머지가 0이면 Buzz를 출력합니다.
  • 그렇지 않으면, 그냥 숫자를 출력합니다.

단계별로 따라하기

이 단계별 튜토리얼을 따라하려면, git 설치, cmake 설치, 그리고 GNU Compiler Collection (GCC) g++ 설치가 필요합니다.

🐰 이 글의 소스 코드는 GitHub에서 사용 가능합니다.

game.cpp라는 이름의 C++ 파일을 만들고, 위의 FizzBuzz 구현으로 내용을 설정하세요.

g++를 사용하여 game이라는 실행 파일을 빌드한 후 실행합니다. 출력은 다음과 같아야 합니다:

$ g++ -std=c++11 game.cpp -o game && ./game
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
...
97
98
Fizz
Buzz

🐰 붐! 당신은 코딩 인터뷰를 깨고 있습니다!

더 나아가기 전에, 마이크로 벤치마킹과 매크로 벤치마킹의 차이점을 논의하는 것이 중요합니다.

마이크로-벤치마킹 vs 매크로-벤치마킹

소프트웨어 벤치마크의 두 가지 주요 카테고리는 마이크로-벤치마크와 매크로-벤치마크입니다. 마이크로-벤치마크는 단위 테스트와 유사한 수준에서 작동합니다. 예를 들어, 단일 숫자에 대해 Fizz, Buzz, 또는 FizzBuzz를 결정하는 함수에 대한 벤치마크는 마이크로-벤치마크가 될 것입니다. 매크로-벤치마크는 통합 테스트와 유사한 수준에서 작동합니다. 예를 들어, 1에서 100까지의 FizzBuzz 게임을 전체적으로 실행하는 함수의 벤치마크는 매크로-벤치마크가 될 것입니다.

일반적으로 가능한 최저 수준의 추상화에서 테스트하는 것이 가장 좋습니다. 벤치마크의 경우, 이를 유지 관리하기 쉽게 만들고, 측정에서의 잡음을 줄이는 데 도움이 됩니다. 그러나, 몇 가지 종단 간 테스트가 전체 시스템이 예상대로 잘 조합되는지 정상 체크에 매우 유용하게 작용하는 것처럼, 매크로-벤치마크가 있으면 소프트웨어를 통한 중요한 경로가 성능을 유지하는지 확인하는데 매우 유용할 수 있습니다.

C++에서의 벤치마킹

C++에서 벤치마킹을 위한 두 가지 인기 있는 옵션은: Google BenchmarkCatch2입니다.

Google Benchmark는 C++용으로 강력하고 다재다능한 벤치마킹 라이브러리로, 개발자들이 코드 성능을 높은 정밀도로 측정할 수 있게 해줍니다. 주요 장점 중 하나는 GoogleTest를 이미 사용하는 프로젝트에 쉽게 통합할 수 있다는 점입니다. Google Benchmark는 CPU 시간, 벽 시계 시간, 메모리 사용량을 측정하는 능력을 포함한 자세한 성능 지표를 제공합니다. 이는 간단한 함수 벤치마크에서부터 복잡하고 매개변수화된 테스트에 이르기까지 다양한 벤치마킹 시나리오를 지원합니다.

Catch2는 C++용으로 현대적이고 헤더 전용인 테스트 프레임워크로, 테스트 작성 및 실행 과정을 간소화합니다. 주된 장점 중 하나는 사용이 용이하다는 점으로, 직관적이고 표현력이 뛰어난 구문을 통해 개발자가 테스트를 빠르고 명확하게 작성할 수 있게 해줍니다. Catch2는 단위 테스트, 통합 테스트, 행위 주도 개발(BDD) 스타일의 테스트 및 기본적인 마이크로 벤치마킹 기능을 포함하여 다양한 종류의 테스트를 지원합니다.

둘 다 Bencher가 지원합니다. 그렇다면 왜 Google Benchmark를 선택할까요? Google Benchmark는 GoogleTest와 매끄럽게 통합되며, GoogleTest는 C++ 생태계에서 사실상의 표준 유닛 테스트 하네스입니다. GoogleTest를 이미 사용하고 있다면, 코드의 대기 시간을 벤치마킹하는 데 Google Benchmark를 사용하는 것을 추천합니다. 즉, Google Benchmark는 실제 시간 측정에 매우 유용합니다.

FizzBuzz 리팩터링

우리의 FizzBuzz 애플리케이션을 테스트하기 위해서는, 프로그램의 main 함수에서 로직을 분리할 필요가 있습니다. 벤치마크 하네스는 main 함수를 벤치마킹할 수 없습니다. 이를 위해서는 몇 가지 변경을 해야 합니다.

새로운 파일 play_game.cpp 내부에 FizzBuzz 로직을 몇 개의 함수로 리팩터링해 봅시다:

play_game.cpp
#include <iostream>
#include <string>
std::string fizz_buzz(int n) {
if (n % 15 == 0) {
return "FizzBuzz";
} else if (n % 3 == 0) {
return "Fizz";
} else if (n % 5 == 0) {
return "Buzz";
} else {
return std::to_string(n);
}
}
void play_game(int n, bool should_print) {
std::string result = fizz_buzz(n);
if (should_print) {
std::cout << result << std::endl;
}
}
  • fizz_buzz: 정수 n을 받아 Fizz, Buzz, FizzBuzz, 또는 숫자 로직을 수행하고 결과를 문자열로 반환합니다.
  • play_game: 정수 n을 받아 fizz_buzz를 해당 숫자로 호출하고, should_printtrue이면 결과를 출력합니다.

이제 play_game.h라는 이름의 헤더 파일을 만들고 play_game 함수 선언을 추가합시다:

play_game.h
#ifndef GAME_H
#define GAME_H
#include <string>
void play_game(int n, bool should_print);
#endif // GAME_H

그런 다음 game.cppmain 함수를 업데이트하여 헤더 파일에서 play_game 함수 정의를 사용합니다:

game.cpp
#include "play_game.h"
int main()
{
for (int i = 1; i <= 100; i++)
{
play_game(i, true);
}
}

프로그램의 main 함수는 1부터 100까지의 숫자를 포함하여 각 숫자에 대해 play_game을 호출하며, should_printtrue로 설정됩니다.

FizzBuzz 벤치마킹

우리의 코드를 벤치마킹하기 위해 먼저 Google Benchmark를 설치해야 합니다.

라이브러리를 복제합니다:

$ git clone https://github.com/google/benchmark.git

새로 복제한 디렉토리로 이동합니다:

$ cd benchmark

cmake를 사용하여 빌드 출력을 배치할 빌드 디렉토리를 생성합니다:

$ cmake -E make_directory "build"

cmake를 사용하여 빌드 시스템 파일을 생성하고 필요한 종속성을 다운로드합니다:

$ cmake -E chdir "build" cmake -DBENCHMARK_DOWNLOAD_DEPENDENCIES=on -DCMAKE_BUILD_TYPE=Release ../

마지막으로, 라이브러리를 빌드합니다:

$ cmake --build "build" --config Release

상위 디렉토리로 돌아갑니다:

cd ..

이제 benchmark_game.cpp라는 새 파일을 만듭니다:

benchmark_game.cpp
#include "play_game.h"
#include <benchmark/benchmark.h>
#include <iostream>
static void BENCHMARK_game(benchmark::State &state)
{
for (auto _ : state)
{
for (int i = 1; i <= 100; i++)
{
play_game(i, false);
}
}
}
BENCHMARK(BENCHMARK_game);
BENCHMARK_MAIN();
  • play_game.h에서 함수 정의를 가져옵니다.
  • Google benchmark 라이브러리 헤더를 가져옵니다.
  • benchmark::State에 대한 참조를 포함하는 BENCHMARK_game이라는 함수 생성.
  • benchmark::State 객체를 반복합니다.
  • 각 반복마다 1부터 100까지 포괄적으로 반복합니다.
    • 현재 번호와 should_printfalse로 설정된 play_game을 호출합니다.
  • BENCHMARK_game 함수를 BENCHMARK 실행기에 전달합니다.
  • BENCHMARK_MAIN을 사용하여 벤치마크를 실행합니다.

이제 우리의 코드를 벤치마킹할 준비가 되었습니다:

$ g++ -std=c++11 -isystem benchmark/include -Lbenchmark/build/src -lbenchmark -lpthread play_game.cpp benchmark_game.cpp -o benchmark_game && ./benchmark_game
2023-10-16T14:00:00-04:00
Running ./benchmark_game
Run on (8 X 24 MHz CPU s)
CPU Caches:
L1 Data 64 KiB
L1 Instruction 128 KiB
L2 Unified 4096 KiB (x8)
Load Average: 5.55, 4.62, 4.69
---------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------
BENCHMARK_game 1698 ns 1688 ns 419979

🐰 상추가 비트를 뒤집자! 우리는 첫 번째 벤치마크 측정값을 얻었습니다!

마지막으로 지친 개발자 머리를 쉴 수 있게 됩니다… 농담입니다. 우리의 사용자들은 새로운 기능을 원합니다!

C++로 FizzBuzzFibonacci 작성하기

우리의 주요 성과 지표(KPIs)가 저조하여, 제품 관리자(PM)는 새로운 기능을 추가하기를 원합니다. 많은 브레인스토밍과 사용자 인터뷰 끝에, 기존의 FizzBuzz로는 충분하지 않다는 결론이 났습니다. 요즘 아이들은 새로운 게임, FizzBuzzFibonacci를 원합니다.

FizzBuzzFibonacci의 규칙은 다음과 같습니다:

1에서 100까지의 정수를 출력하는 프로그램을 작성하세요 :

  • 3의 배수는 Fizz를 출력합니다.
  • 5의 배수는 Buzz를 출력합니다.
  • 3과 5의 배수 모두인 경우에는 FizzBuzz를 출력합니다.
  • 피보나치 수열의 일부인 숫자는 Fibonacci만 출력합니다.
  • 그 외의 모든 숫자는 숫자를 출력합니다.

피보나치 수열은 각 숫자가 그 이전 두 숫자의 합계인 수열입니다. 예를 들어, 01에서 시작하여 피보나치 수열의 다음 숫자는 1입니다. 그 다음에는 2, 3, 5, 8 등이 이어집니다. 피보나치 수열의 일부인 숫자를 피보나치 수라고 합니다. 따라서 우리는 피보나치 수를 감지하는 함수를 작성해야 합니다.

피보나치 수열을 작성하는 방법은 많이 있습니다 마찬가지로 피보나치 수를 감지하는 방법도 많습니다. 따라서 제가 가장 선호하는 방법을 선택하겠습니다:

play_game.cpp
bool is_fibonacci_number(int n)
{
for (int i = 0; i <= n; ++i)
{
int previous = 0, current = 1;
while (current < i)
{
int next = previous + current;
previous = current;
current = next;
}
if (current == n)
{
return true;
}
}
return false;
}
  • 정수를 입력으로 받아 불리언을 반환하는 is_fibonacci_number라는 함수 생성.
  • 주어진 숫자 n까지 0부터 모든 숫자에 대해 반복합니다.
  • 피보나치 수열을 01로 시작하여 각각 previouscurrent 숫자로 초기화합니다.
  • current 숫자가 현재 반복하는 i보다 작을 때까지 반복합니다.
  • previouscurrent 숫자를 더하여 next 숫자를 얻습니다.
  • previous 숫자를 current 숫자로 업데이트합니다.
  • current 숫자를 next 숫자로 업데이트합니다.
  • current가 주어진 숫자 n보다 크거나 같아지면 루프를 종료합니다.
  • current 숫자가 주어진 숫자 n과 같은지 확인하고 그렇다면 true를 반환합니다.
  • 그렇지 않으면 false를 반환합니다.

이제 fizz_buzz 함수를 업데이트할 필요가 있습니다:

play_game.cpp
std::string fizz_buzz_fibonacci(int n)
{
if (is_fibonacci_number(n))
{
return "Fibonacci";
}
else if (n % 15 == 0)
{
return "FizzBuzz";
}
else if (n % 3 == 0)
{
return "Fizz";
}
else if (n % 5 == 0)
{
return "Buzz";
}
else
{
return std::to_string(n);
}
}
  • 더 설명적인 이름으로 바꾸기 위해 fizz_buzz 함수를 fizz_buzz_fibonacci로 이름을 변경합니다.
  • is_fibonacci_number 헬퍼 함수를 호출합니다.
  • is_fibonacci_number의 결과가 true이면 Fibonacci를 반환합니다.
  • is_fibonacci_number의 결과가 false이면 동일한 Fizz, Buzz, FizzBuzz, 숫자 논리를 수행하여 결과를 반환합니다.

fizz_buzzfizz_buzz_fibonacci로 이름을 변경했기 때문에 play_game 함수도 업데이트해야 합니다:

play_game.cpp
void play_game(int n, bool should_print) {
std::string result = fizz_buzz_fibonacci(n);
if (should_print) {
std::cout << result << std::endl;
}
}

main 함수와 BENCHMARK_game 함수는 그대로 유지할 수 있습니다.

피즈버즈피보나치 벤치마킹

이제 벤치마크를 다시 실행할 수 있습니다:

$ g++ -std=c++11 -isystem benchmark/include -Lbenchmark/build/src -lbenchmark -lpthread play_game.cpp benchmark_game.cpp -o benchmark_game && ./benchmark_game
2023-10-16T15:00:00-04:00
Running ./benchmark_game
Run on (8 X 24 MHz CPU s)
CPU Caches:
L1 Data 64 KiB
L1 Instruction 128 KiB
L2 Unified 4096 KiB (x8)
Load Average: 4.34, 5.75, 4.71
---------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------
BENCHMARK_game 56190 ns 56054 ns 12280

터미널 기록을 스크롤하면서, 피즈버즈와 피즈버즈피보나치 게임의 성능을 대략적으로 비교할 수 있습니다: 1698 ns vs 56190 ns. 여러분의 결과는 저와 약간 다를 수 있습니다. 하지만 두 게임 간의 차이는 아마도 50배 정도일 것입니다. 저에게는 충분히 좋아 보입니다! 게임에 _피보나치_라는 거창한 기능을 추가하는 것치고는 꽤 잘한 것 같습니다. 아이들이 좋아할 것입니다!

C++로 FizzBuzzFibonacci 확장하기

우리 게임이 성공했습니다! 아이들이 정말로 FizzBuzzFibonacci를 즐깁니다. 그래서 경영진에서 후속작을 원한다는 소식을 들었습니다. 하지만 현대 사회에서는 단일 구매가 아닌 연간 반복 수익(ARR)이 필요합니다! 우리 게임에 대한 새로운 비전은 무한한 끝으로, 더 이상 1100 (포함한) 사이에 머물지 않습니다. 아니요, 우리는 새로운 경계를 찾아 나아갑니다!

Open World FizzBuzzFibonacci의 규칙은 다음과 같습니다:

양의 정수를 입력으로 받아 다음을 출력하는 프로그램을 작성하십시오:

  • 세의 배수인 경우 Fizz를 출력합니다
  • 다섯의 배수인 경우 Buzz를 출력합니다
  • 세와 다섯의 공배수인 경우 FizzBuzz를 출력합니다
  • 피보나치 수열에 포함되는 숫자인 경우 Fibonacci만 출력합니다
  • 그 외의 숫자는 숫자 그대로 출력합니다.

우리 게임이 임의의 숫자에 대해 작동하려면 명령 줄 인수를 받아야 합니다. main 함수를 다음과 같이 업데이트하십시오:

game.cpp
#include "play_game.h"
#include <iostream>
#include <cstdlib>
int main(int argc, char *argv[])
{
if (argc > 1 && std::isdigit(argv[1][0]))
{
int i = std::atoi(argv[1]);
play_game(i, true);
}
else
{
std::cout << "Please, enter a positive integer to play..." << std::endl;
}
return 0;
}
  • argcargv를 받아들이도록 main 함수를 업데이트합니다.
  • 게임에 전달된 첫 번째 인수를 가져와 숫자인지 확인합니다.
    • 그렇다면 첫 번째 인수를 정수 i로 파싱합니다.
    • 새로 파싱된 정수 i로 게임을 진행합니다.
  • 파싱에 실패하거나 인수가 전달되지 않으면 유효한 입력을 요청하는 기본 설정으로 돌아갑니다.

이제 임의의 숫자로 게임을 즐길 수 있습니다! game 실행 파일을 다시 컴파일한 다음 숫자를 따라 실행 파일을 실행하여 게임을 진행하십시오:

$ g++ -std=c++11 game.cpp play_game.cpp -o game
$ ./game 9
Fizz
$ ./game 10
Buzz
$ ./game 13
Fibonacci

또는 잘못된 숫자를 제공하거나 생략한 경우:

$ ./game
Please, enter a positive integer to play...
$ ./game bad
Please, enter a positive integer to play...

와우, 정말 철저한 테스트였습니다! CI가 통과했습니다. 상사도 기뻐합니다. 출시합시다! 🚀


SpongeBob SquarePants Three Weeks Later
This is Fine meme

🐰 … 아마 당신의 경력의 끝일까요?


농담이었어요! 모든 것이 불타고 있어요! 🔥

처음에는 모든 것이 잘 진행된 것처럼 보였어요. 그런데 토요일 새벽 2시 7분에 내 호출기가 울렸습니다:

📟 당신의 게임이 불타고 있어요! 🔥

침대에서 뛰어나와서 무슨 일이 일어나고 있는지 알아내려고 노력했습니다. 로그를 검색하려고 했지만, 모든 것이 계속 충돌해서 검색이 어려웠습니다. 드디어 문제를 찾았습니다. 아이들이었습니다. 아이들이 우리 게임을 너무 좋아해서 백만 번이나 플레이하고 있었습니다! 기백한 두뇌를 깨우고, 두 개의 새로운 벤치마크를 추가했습니다:

benchmark_game.cpp
static void BENCHMARK_game_100(benchmark::State &state)
{
for (auto _ : state)
{
play_game(100, false);
}
}
static void BENCHMARK_game_1_000_000(benchmark::State &state)
{
for (auto _ : state)
{
play_game(1000000, false);
}
}
BENCHMARK(BENCHMARK_game_100);
BENCHMARK(BENCHMARK_game_1_000_000);
  • 숫자 100으로 게임을 실행하는 마이크로 벤치마크 BENCHMARK_game_100
  • 숫자 1,000,000으로 게임을 실행하는 마이크로 벤치마크 BENCHMARK_game_1_000_000

실행 결과는 다음과 같았습니다:

$ g++ -std=c++11 -isystem benchmark/include -Lbenchmark/build/src -lbenchmark -lpthread play_game.cpp benchmark_game.cpp -o benchmark_game && ./benchmark_game
2023-11-04T03:00:00-04:00
Running ./benchmark_game
Run on (8 X 24 MHz CPU s)
CPU Caches:
L1 Data 64 KiB
L1 Instruction 128 KiB
L2 Unified 4096 KiB (x8)
Load Average: 4.98, 5.75, 4.96
-------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------
BENCHMARK_game 75547 ns 59280 ns 12560
BENCHMARK_game_100 1249 ns 1243 ns 564689

기다려 보세요… 기다려 보세요…

BENCHMARK_game_1_000_000 110879642 ns 43628118 ns 17

뭐라고! 1,249 ns x 10,00012,490,000 ns 여야 하는데 110,879,642 ns 이라니 🤯 피보나치 수열 코드가 기능적으로 올바르지만, 성능 버그가 어딘가에 있는 게 분명합니다.

C++에서 FizzBuzzFibonacci 수정하기

is_fibonacci_number 함수에 대해 다시 살펴보겠습니다:

play_game.cpp
bool is_fibonacci_number(int n)
{
for (int i = 0; i <= n; ++i)
{
int previous = 0, current = 1;
while (current < i)
{
int next = previous + current;
previous = current;
current = next;
}
if (current == n)
{
return true;
}
}
return false;
}

이제 성능을 생각해보니 불필요한 반복문이 있다는 것을 깨달았습니다. for (int i = 0; i <= n; ++i) 반복문을 완전히 없애고 단순히 current 값을 주어진 숫자(n)와 비교하면 됩니다 🤦

play_game.cpp
bool is_fibonacci_number(int n)
{
int previous = 0, current = 1;
while (current < n)
{
int next = previous + current;
previous = current;
current = next;
}
return current == n;
}
  • is_fibonacci_number 함수를 업데이트합니다.
  • 01로 시작하는 피보나치 수열을 각각 previouscurrent라는 숫자로 초기화합니다.
  • current 숫자가 주어진 숫자 n보다 작은 동안 반복합니다.
  • previouscurrent 숫자를 더하여 next 숫자를 얻습니다.
  • previous 숫자를 current 숫자로 업데이트합니다.
  • current 숫자를 next 숫자로 업데이트합니다.
  • current가 주어진 숫자 n보다 크거나 같아지면 루프를 종료합니다.
  • current 숫자가 주어진 숫자 n과 같은지 확인하고 그 결과를 반환합니다.

이제 벤치마크를 다시 실행해서 어떻게 되었는지 봅시다:

$ g++ -std=c++11 -isystem benchmark/include -Lbenchmark/build/src -lbenchmark -lpthread play_game.cpp benchmark_game.cpp -o benchmark_game && ./benchmark_game
2023-11-04T05:00:00-04:00
Running ./benchmark_game
Run on (8 X 24 MHz CPU s)
CPU Caches:
L1 Data 64 KiB
L1 Instruction 128 KiB
L2 Unified 4096 KiB (x8)
Load Average: 4.69, 5.02, 4.78
-------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------
BENCHMARK_game 2914 ns 2913 ns 242382
BENCHMARK_game_100 34.4 ns 34.3 ns 20322076
BENCHMARK_game_1_000_000 61.6 ns 61.6 ns 11346874

와! 우리의 BENCHMARK_game 벤치마크가 원래의 FizzBuzz 수준으로 돌아왔습니다. 정확히 어떤 점수가 나왔는지 기억하고 싶습니다. 벌써 3주가 지났네요. 터미널 기록은 그 정도까지 가지 않고, Google Benchmark도 결과를 저장하지 않습니다. 하지만 비슷한 것 같습니다!

BENCHMARK_game_100 벤치마크는 거의 50배 감소하여 34.4 ns를 기록했습니다. 그리고 BENCHMARK_game_1_000_000 벤치마크는 1,500,000배 이상 감소했습니다! 110,879,642 ns에서 61.6 ns로!

🐰 다행히도 이 성능 버그가 프로덕션에 반영되기 전에 잡았습니다… 아, 맞다. 아니에요…

CI에서 성능 저하를 포착하세요

나의 작은 성능 버그로 인해 우리 게임이 받은 부정적인 리뷰들에 대해 경영진들은 만족하지 않았습니다. 그들은 저에게 다시는 그런 일이 발생하지 않도록 하라고 말했고, 어떻게 해야 하는지 물었을 때, 그들은 그냥 다시는 하지 말라고만 말했습니다. 어떻게 그렇게 관리해야 하지요‽

다행스럽게도, 저는 Bencher라는 놀라운 오픈 소스 툴을 발견했습니다. 아주 관대한 무료 티어가 있으므로 저는 개인 프로젝트에 Bencher Cloud를 그냥 사용할 수 있습니다. 그리고 회사에서는 모든 것이 우리의 프라이빗 클라우드에 있어야 하므로, 저는 Bencher Self-Hosted를 사용하기 시작했습니다.

Bencher는 내장 어댑터가 있으므로 CI에 통합하기 쉽습니다. 빠른 시작 가이드를 따른 후, Bencher를 이용해 벤치마크를 실행하고 추적할 수 있습니다.

$ g++ -std=c++11 -isystem benchmark/include -Lbenchmark/build/src -lbenchmark -lpthread play_game.cpp benchmark_game.cpp -o benchmark_game
$ bencher run --adapter cpp_google "./benchmark_game --benchmark_format=json"
{
"context": {
"date": "2023-10-16T16:00:00-04:00",
"host_name": "bencher",
"executable": "./benchmark_game",
"num_cpus": 8,
"mhz_per_cpu": 24,
"cpu_scaling_enabled": false,
...
View results:
- BENCHMARK_game (Latency): https://bencher.dev/console/projects/game/perf?measures=52507e04-ffd9-4021-b141-7d4b9f1e9194&branches=3a27b3ce-225c-4076-af7c-75adbc34ef9a&testbeds=bc05ed88-74c1-430d-b96a-5394fdd18bb0&benchmarks=077449e5-5b45-4c00-bdfb-3a277413180d&start_time=1697224006000&end_time=1699816009000&upper_boundary=true
- BENCHMARK_game_100 (Latency): https://bencher.dev/console/projects/game/perf?measures=52507e04-ffd9-4021-b141-7d4b9f1e9194&branches=3a27b3ce-225c-4076-af7c-75adbc34ef9a&testbeds=bc05ed88-74c1-430d-b96a-5394fdd18bb0&benchmarks=96508869-4fa2-44ac-8e60-b635b83a17b7&start_time=1697224006000&end_time=1699816009000&upper_boundary=true
- BENCHMARK_game_1_000_000 (Latency): https://bencher.dev/console/projects/game/perf?measures=52507e04-ffd9-4021-b141-7d4b9f1e9194&branches=3a27b3ce-225c-4076-af7c-75adbc34ef9a&testbeds=bc05ed88-74c1-430d-b96a-5394fdd18bb0&benchmarks=ff014217-4570-42ea-8813-6ed0284500a4&start_time=1697224006000&end_time=1699816009000&upper_boundary=true

이 멋진 시간 여행 장치를 통해, 친절한 토끼가 내게 준 것을 활용하여, Bencher를 계속 사용했더라면 무슨 일이 일어났을지 되돌려보았습니다. 처음으로 버그가 있는 FizzBuzzFibonacci 구현을 푸시한 곳을 볼 수 있습니다. 나는 즉시 내 풀 요청에 대해 CI에서 실패를 받았습니다. 그날 저녁, 저는 그 불필요하고, 추가적인 반복문을 제거함으로써 성능 버그를 수정했습니다. 화재는 없습니다. 그저 행복한 사용자들만 있습니다.

Bencher: 지속적인 벤치마킹

🐰 Bencher

Bencher는 지속적인 벤치마킹 도구 모음입니다. 성능 회귀가 사용자에게 영향을 미친 경험이 있나요? Bencher가 그런 일이 일어나는 것을 막을 수 있었습니다. Bencher를 이용하면 성능 회귀를 상용 환경으로 이동하기 전에 탐지하고 예방할 수 있습니다.

  • 실행: 기존 벤치마킹 도구를 사용하여 로컬 또는 CI에서 벤치마크를 실행합니다. bencher CLI는 기존 벤치마킹 하네스를 감싸고 결과를 저장합니다.
  • 추적: 벤치마크 결과를 시간이 지남에 따라 추적합니다. 소스 브랜치, 테스트 베드, 측정 기반의 Bencher 웹 콘솔을 사용하여 결과를 모니터링, 쿼리, 그래프로 만듭니다.
  • 캐치: CI에서 성능 회귀를 잡아냅니다. Bencher는 최첨단, 사용자 정의 가능한 분석을 사용하여 상용 환경으로 가기 전에 성능 회귀를 탐지합니다.

단위 테스트가 CI에서 기능 회귀를 방지하기 위해 실행되는 것처럼, 벤치마크는 Bencher와 함께 CI에서 실행되어 성능 회귀를 방지해야 합니다. 성능 버그도 버그입니다!

CI에서 성능 회귀를 잡아내기 시작하세요 - Bencher Cloud를 무료로 시도해보세요.

🤖 이 문서는 OpenAI GPT-4에 의해 자동으로 생성되었습니다. 정확하지 않을 수도 있고 오류가 있을 수도 있습니다. 오류를 발견하면 GitHub에서 문제를 열어주세요.