pytest-benchmark를 사용하여 Python 코드를 벤치마킹하는 방법

Everett Pompeii

Everett Pompeii


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

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

파이썬으로 FizzBuzz 작성하기

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

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

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

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

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

for i in range(1, 101):
if n % 15 == 0:
print("FizzBuzz")
elif n % 3 == 0:
print("Fizz")
elif n % 5 == 0:
print("Buzz")
else:
print(i)
  • 1에서 100까지 반복합니다. 101의 범위를 사용하세요.
  • 각 숫자에 대해 35로 나눈 나머지(모듈러스)를 계산합니다.
  • 나머지가 0이면 해당 수는 주어진 인수의 배수입니다.
    • 나머지가 015의 배수인 경우, FizzBuzz를 출력합니다.
    • 나머지가 03의 배수인 경우, Fizz를 출력합니다.
    • 나머지가 05의 배수인 경우, Buzz를 출력합니다.
  • 그렇지 않으면 숫자 자체를 출력합니다.

단계별 따라하기

이 단계별 튜토리얼을 따라하기 위해서는 Python 설치pipenv 설치가 필요합니다.

🐰 이 게시물의 소스 코드는 GitHub에서 제공됩니다.

game.py라는 이름의 Python 파일을 만들고, 위의 FizzBuzz 구현을 그 내용으로 설정하십시오.

그러고 나서 python game.py를 실행하십시오. 출력은 다음과 같아야 합니다:

$ python game.py
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 게임을 전체적으로 실행하는 함수의 벤치마크는 매크로-벤치마크가 될 것입니다.

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

Python에서의 벤치마킹

파이썬에서 벤치마킹을 위한 두 가지 인기 있는 옵션은 다음과 같습니다: pytest-benchmark 그리고 airspeed velocity (asv)

pytest-benchmark는 인기 있는 pytest 테스트 프레임워크와 통합된 강력한 벤치마킹 도구입니다. 개발자가 단위 테스트와 함께 벤치마크를 실행하여 코드의 성능을 측정하고 비교할 수 있도록 합니다. 사용자는 로컬에서 쉽게 벤치마크 결과를 비교하고 JSON을 포함한 다양한 형식으로 결과를 내보낼 수 있습니다.

airspeed velocity (asv)는 파이썬 생태계에서 또 다른 고급 벤치마킹 도구입니다. asv의 주요 이점 중 하나는 상세하고 인터랙티브한 HTML 보고서를 생성할 수 있다는 점으로, 이는 성능 추세를 시각화하고 리그레션을 식별하기 쉽게 만듭니다. 또한, asv상대 연속 벤치마킹을 기본적으로 지원합니다.

둘 다 벤쳐의 지원을 받습니다. 그렇다면 왜 pytest-benchmark를 선택해야 할까요? pytest-benchmark는 Python 생태계에서 사실상 표준 유닛 테스트 하니스인 pytest와 매끄럽게 통합됩니다. 이미 pytest를 사용하고 있다면 코드의 지연 시간을 벤치마킹하기 위해 pytest-benchmark를 사용하는 것이 좋습니다. 즉, pytest-benchmark는 경과 시간(wall clock time)을 측정하는 데 탁월합니다.

FizzBuzz 리팩토링

우리의 FizzBuzz 애플리케이션을 테스트하기 위해, 로직을 프로그램의 메인 실행과 분리해야 합니다. 벤치마크 하네스는 메인 실행을 벤치마킹할 수 없습니다. 이를 위해 약간의 변경이 필요합니다.

FizzBuzz 로직을 몇 가지 함수로 리팩토링해 봅시다:

def play_game(n, should_print):
result = fizz_buzz(n)
if should_print:
print(result)
return result
def fizz_buzz(n):
if n % 15 == 0:
return "FizzBuzz"
elif n % 3 == 0:
return "Fizz"
elif n % 5 == 0:
return "Buzz"
else:
return str(n)
  • play_game: 정수 n을 입력으로 받아 fizz_buzz를 그 숫자로 호출하며, should_printTrue일 경우 결과를 출력합니다.
  • fizz_buzz: 정수 n을 입력으로 받아 실제 Fizz, Buzz, FizzBuzz 또는 숫자 로직을 수행하고 결과를 문자열로 반환합니다.

그런 다음 메인 실행을 다음과 같이 업데이트합니다:

game.py
for i in range(1, 101):
play_game(i, True)

우리 프로그램의 메인 실행은 1부터 100까지의 숫자를 포함하여 반복하며, 각 숫자에 대해 should_printTrue로 설정된 상태로 play_game을 호출합니다.

피즈버즈 벤치마킹

코드를 벤치마크하기 위해, 벤치마크를 실행하는 테스트 함수를 생성해야 합니다. game.py의 맨 아래에 다음 코드를 추가하세요:

def test_game(benchmark):
def run_game():
for i in range(1, 101):
play_game(i, False)
benchmark(run_game)
  • pytest-benchmarkbenchmark 픽스처를 받는 test_game이라는 이름의 함수를 생성합니다.
  • 1부터 100까지 포괄적으로 반복하는 run_game 함수를 생성합니다.
    • 각 숫자에 대해, should_printFalse로 설정하여 play_game을 호출합니다.
  • benchmark 러너에 run_game 함수를 전달합니다.

이제 벤치마크를 실행하기 위해 프로젝트를 설정해야 합니다.

pipenv로 새로운 가상 환경을 생성하세요:

$ pipenv shell
Creating a Pipfile for this project...
Launching subshell in virtual environment...
source /usr/bencher/.local/share/virtualenvs/test-xnizGmtA/bin/activate

새로운 pipenv 환경 내에 pytest-benchmark를 설치하세요:

$ pipenv install pytest-benchmark
Creating a Pipfile for this project...
Installing pytest-benchmark...
Resolving pytest-benchmark...
Added pytest-benchmark to Pipfile's [packages] ...
✔ Installation Succeeded
Pipfile.lock not found, creating...
Locking [packages] dependencies...
Building requirements...
Resolving dependencies...
✔ Success!
Locking [dev-packages] dependencies...
Updated Pipfile.lock (be953321071292b6175f231c7e2e835a3cd26169a0d52b7b781b344d65e8cce3)!
Installing dependencies from Pipfile.lock (e8cce3)...

이제 코드를 벤치마크할 준비가 되었습니다. pytest game.py를 실행하세요:

$ pytest game.py
======================================================= test session starts ========================================================
platform darwin -- Python 3.12.7, pytest-8.3.3, pluggy-1.5.0
benchmark: 4.0.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /usr/bencher/examples/python/pytest_benchmark
plugins: benchmark-4.0.0
collected 1 item
game.py . [100%]
------------------------------------------------- benchmark: 1 tests -------------------------------------------------
Name (time in us) Min Max Mean StdDev Median IQR Outliers OPS (Kops/s) Rounds Iterations
----------------------------------------------------------------------------------------------------------------------
test_game 10.5416 237.7499 10.8307 1.3958 10.7088 0.1248 191;10096 92.3304 57280 1
----------------------------------------------------------------------------------------------------------------------
Legend:
Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
OPS: Operations Per Second, computed as 1 / Mean
======================================================== 1 passed in 1.68s =========================================================

🐰 양배추가 많이 들어간 비트를 즐기세요! 첫 벤치마크 메트릭이 나왔습니다!

마지막으로, 피곤한 개발자의 머리를 쉬게 할 수 있습니다… 농담입니다. 사용자들은 새로운 기능을 원합니다!

파이썬으로 FizzBuzzFibonacci 작성하기

우리의 핵심 성과 지표(KPIs)가 떨어졌습니다. 그래서 제품 관리자(PM)는 새로운 기능을 추가하기를 원합니다. 많은 브레인스토밍과 사용자 인터뷰 후에, 전통적인 FizzBuzz만으로는 충분하지 않다는 결론에 도달했습니다. 요즘 아이들은 새로운 게임인 FizzBuzzFibonacci를 원합니다.

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

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

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

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

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

def is_fibonacci_number(n):
for i in range(n + 1):
previous, current = 0, 1
while current < i:
next_value = previous + current
previous = current
current = next_value
if current == n:
return True
return False
  • 정수를 입력받아 부울 값을 반환하는 is_fibonacci_number라는 함수를 만듭니다.
  • 주어진 숫자 n까지 숫자 0에서부터 반복합니다.
  • 이전 숫자와 현재 숫자로 각각 01로 시작하는 피보나치 수열을 초기화합니다.
  • 현재 숫자가 현재 반복 i보다 작을 동안 반복합니다.
  • 이전 숫자와 현재 숫자를 더해 next_value 숫자를 얻습니다.
  • 이전 숫자를 현재 숫자로 업데이트합니다.
  • 현재 숫자를 next_value 숫자로 업데이트합니다.
  • 현재가 주어진 숫자 n보다 크거나 같아질 때 루프를 종료합니다.
  • 현재 숫자가 주어진 숫자 n과 같은지 확인하고 만약 그렇다면 True를 반환합니다.
  • 그렇지 않으면 False를 반환합니다.

이제 fizz_buzz 함수를 업데이트해야 합니다:

def fizz_buzz_fibonacci(n):
if is_fibonacci_number(n):
return "Fibonacci"
elif n % 15 == 0:
return "FizzBuzz"
elif n % 3 == 0:
return "Fizz"
elif n % 5 == 0:
return "Buzz"
else:
return str(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 함수도 업데이트해야 합니다:

def play_game(n, should_print):
result = fizz_buzz_fibonacci(n)
if should_print:
print(result)
return result

주요 실행과 test_game 함수 둘 다 정확히 동일하게 유지할 수 있습니다.

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

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

$ pytest game.py
======================================================= test session starts ========================================================
platform darwin -- Python 3.12.7, pytest-8.3.3, pluggy-1.5.0
benchmark: 4.0.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /usr/bencher/examples/python/pytest_benchmark
plugins: benchmark-4.0.0
collected 1 item
game.py . [100%]
--------------------------------------------------- benchmark: 1 tests --------------------------------------------------
Name (time in us) Min Max Mean StdDev Median IQR Outliers OPS (Kops/s) Rounds Iterations
-------------------------------------------------------------------------------------------------------------------------
test_game 726.9592 848.2919 735.5682 13.4925 731.4999 4.7078 146;192 1.3595 1299 1
-------------------------------------------------------------------------------------------------------------------------
Legend:
Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
OPS: Operations Per Second, computed as 1 / Mean
======================================================== 1 passed in 1.97s =========================================================

터미널 기록을 다시 스크롤하여 피즈버즈와 피즈버즈피보나치 게임의 성능을 대략 비교할 수 있습니다: 10.8307 us vs 735.5682 us. 여러분의 숫자는 제 것과 약간 다를 수 있습니다. 하지만 두 게임 간의 차이는 아마도 50배 정도일 것입니다. 게임에 _피보나치_라는 멋지게 들리는 기능을 추가하기 위해서는 그 정도면 좋다고 생각합니다. 아이들이 좋아할 것입니다!

Python에서 FizzBuzzFibonacci 확대하기

우리의 게임이 대박을 쳤습니다! 아이들이 정말로 FizzBuzzFibonacci 게임을 즐겨합니다. 그렇게 해서 경영진으로부터 후속작을 만들라는 요청이 내려왔습니다. 하지만 현대 사회에서는, 일회성 구매가 아닌 연간 경상 수익(ARR)이 필요합니다! 우리 게임의 새로운 비전은 개방형 게임이라는 것으로, 더 이상 1100 사이(포함)에서 머무르지 않습니다. 아니요, 우리는 새로운 경계를 향해 나아갑니다!

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

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

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

우리 게임이 어떤 숫자에서든 작동하려면 명령줄 인수를 받아야 합니다. 주 실행을 다음과 같이 업데이트하세요:

game.py
import sys
args = sys.argv
if len(args) > 1 and args[1].isdigit():
i = int(args[1])
play_game(i, True)
else:
print("Please, enter a positive integer to play...")
  • sys 패키지를 가져옵니다.
  • 명령줄에서 우리 게임으로 전달된 모든 인수(args)를 수집합니다.
  • 우리 게임에 전달된 첫 번째 인수를 가져와서 숫자인지 확인합니다.
    • 숫자라면, 첫 번째 인수를 정수 i로 파싱합니다.
    • 새로 파싱된 정수 i로 우리의 게임을 실행합니다.
  • 파싱에 실패하거나 어떤 인수도 전달되지 않는다면, 유효한 입력을 요청하는 기본값으로 설정합니다.

이제 어떤 숫자로든 우리의 게임을 플레이할 수 있습니다! 게임을 하려면 python game.py 명령 뒤에 정수를 입력하세요:

$ python game.py 9
Fizz
$ python game.py 10
Buzz
$ python game.py 13
Fibonacci

그리고 잘못된 수를 생략하거나 제공하는 경우:

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

와우, 그것은 진정한 철저한 테스트였습니다! CI가 통과했습니다. 상사들도 기뻐합니다. 배포합시다! 🚀


SpongeBob SquarePants Three Weeks Later
This is Fine meme

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


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

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

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

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

def test_game_100(benchmark):
def run_game():
play_game(100, False)
benchmark(run_game)
def test_game_1_000_000(benchmark):
def run_game():
play_game(1_000_000, False)
benchmark(run_game)
  • 숫자 백(100)을 사용하여 게임을 실행하는 마이크로-벤치마크 test_game_100
  • 숫자 백만(1_000_000)을 사용하여 게임을 실행하는 마이크로-벤치마크 test_game_1_000_000

제가 실행했을 때, 이런 결과가 나왔습니다:

$ pytest game.py
======================================================= test session starts ========================================================
platform darwin -- Python 3.12.7, pytest-8.3.3, pluggy-1.5.0
benchmark: 4.0.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /usr/bencher/examples/python/pytest_benchmark
plugins: benchmark-4.0.0
collected 3 items
game.py ... [100%]

잠깐만 기다려 보세요…

-------------------------------------------------------------------------------------------------- benchmark: 3 tests --------------------------------------------------------------------------------------------------
Name (time in us) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
test_game_100 15.4166 (1.0) 112.8749 (1.0) 15.8470 (1.0) 1.1725 (1.0) 15.6672 (1.0) 0.1672 (1.0) 1276;7201 63,103.3078 (1.0) 58970 1
test_game 727.0002 (47.16) 1,074.3327 (9.52) 754.3231 (47.60) 33.2047 (28.32) 748.9999 (47.81) 33.7283 (201.76) 134;54 1,325.6918 (0.02) 1319 1
test_game_1_000_000 565,232.3328 (>1000.0) 579,829.1252 (>1000.0) 571,684.6334 (>1000.0) 6,365.1577 (>1000.0) 568,294.3747 (>1000.0) 10,454.0113 (>1000.0) 2;0 1.7492 (0.00) 5 1
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Legend:
Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
OPS: Operations Per Second, computed as 1 / Mean
======================================================== 3 passed in 7.01s =========================================================

뭐라고요! 15.8470 us x 1,000이면 15,847.0 us이 되어야 하는데 571,684.6334 us라니 🤯 비록 피보나치 수열 코드가 기능적으로는 올바르게 작동했지만, 어딘가에 성능 버그가 있는 것이 틀림없습니다.

Python에서 FizzBuzzFibonacci 수정하기

is_fibonacci_number 함수를 다시 한 번 살펴봅시다:

def is_fibonacci_number(n):
for i in range(n + 1):
previous, current = 0, 1
while current < i:
next_value = previous + current
previous = current
current = next_value
if current == n:
return True
return False

이제 성능에 대해 생각해보니, 불필요한 추가 루프가 있다는 것을 깨달았습니다. for i in range(n + 1): 루프를 완전히 제거하고 주어진 숫자(n)와 current 값을 비교할 수 있습니다. 🤦

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

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

$ pytest game.py
======================================================= test session starts ========================================================
platform darwin -- Python 3.12.7, pytest-8.3.3, pluggy-1.5.0
benchmark: 4.0.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /usr/bencher/examples/python/pytest_benchmark
plugins: benchmark-4.0.0
collected 3 items
game.py ... [100%]
------------------------------------------------------------------------------------------------ benchmark: 3 tests ------------------------------------------------------------------------------------------------
Name (time in ns) Min Max Mean StdDev Median IQR Outliers OPS (Kops/s) Rounds Iterations
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
test_game_100 309.8685 (1.0) 40,197.8614 (2.38) 322.0815 (1.0) 101.7570 (1.0) 320.2877 (1.0) 5.1805 (1.0) 321;12616 3,104.8046 (1.0) 195120 16
test_game_1_000_000 724.9881 (2.34) 16,912.4920 (1.0) 753.1445 (2.34) 121.0458 (1.19) 741.7053 (2.32) 12.4797 (2.41) 656;13698 1,327.7664 (0.43) 123073 10
test_game 26,958.9946 (87.00) 129,667.1107 (7.67) 27,448.7719 (85.22) 1,555.0003 (15.28) 27,291.9424 (85.21) 165.7754 (32.00) 479;2372 36.4315 (0.01) 25918 1
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Legend:
Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
OPS: Operations Per Second, computed as 1 / Mean
======================================================== 3 passed in 3.99s =========================================================

오, 세상에! 우리의 test_game 벤치마크가 원래의 FizzBuzz에 근접한 수준으로 다시 내려왔습니다. 그 점수가 정확히 무엇이었는지 기억할 수 있었으면 좋겠네요. 벌써 3주나 지났네요. 터미널 히스토리는 그렇게 오래가지 않거든요. 그리고 pytest-benchmark는 우리가 요청해야만 결과를 저장합니다. 하지만 거의 근접했다고 생각합니다!

test_game_100 벤치마크는 거의 50배나 내려가서 322.0815 ns가 되었습니다. 그리고 test_game_1_000_000 벤치마크는 500,000배 이상 내려갔습니다! 571,684,633.4 ns753.1445 ns로!

🐰 적어도 이 성능 버그가 프로덕션에 배포되기 전에 발견했다는 사실에 감사해야겠죠… 아, 맞다. 그냥 잊으세요…

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

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

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

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

$ bencher run --adapter python_pytest --file results.json "pytest --benchmark-json results.json game.py"
======================================================= test session starts ========================================================
platform darwin -- Python 3.12.7, pytest-8.3.3, pluggy-1.5.0
benchmark: 4.0.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /usr/bencher/examples/python/pytest_benchmark
plugins: benchmark-4.0.0
collected 3 items
game.py ...
...
View results:
- test_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
- test_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
- test_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에서 문제를 열어주세요.