pytest-benchmark를 사용하여 Python 코드를 벤치마킹하는 방법
Everett Pompeii
벤치마킹이란 무엇인가요?
벤치마킹은 코드의 성능을 테스트하여 코드가 얼마나 빠르게(지연시간) 또는 얼마나 많은 양(처리량)의 작업을 수행할 수 있는지를 확인하는 실습입니다. 이는 소프트웨어 개발에서 종종 간과되지만, 빠르고 성능 좋은 코드를 작성하고 유지하기 위해 매우 중요한 단계입니다. 벤치마킹은 개발자가 다양한 작업 부하와 조건에서 코드가 얼마나 잘 수행되는지를 이해하는 데 필요한 메트릭을 제공합니다. 기능 회귀를 방지하기 위해 단위 테스트와 통합 테스트를 작성하는 것과 같은 이유로, 성능 회귀를 방지하기 위해 벤치마크를 작성해야 합니다. 성능 버그도 버그입니다!
파이썬으로 FizzBuzz 작성하기
벤치마크를 작성하기 위해서는 벤치마크할 소스 코드가 필요합니다. 처음으로 매우 간단한 프로그램인, FizzBuzz를 작성해 봅시다.
FizzBuzz의 규칙은 다음과 같습니다:
1
부터100
까지의 정수를 출력하는 프로그램을 작성하십시오:
- 3의 배수인 경우
Fizz
를 출력합니다- 5의 배수인 경우
Buzz
를 출력합니다- 3과 5의 공배수인 경우
FizzBuzz
를 출력합니다- 그 외의 경우에는 숫자를 출력합니다.
FizzBuzz를 작성하는 방법은 여러 가지가 있습니다. 그래서 저는 제가 가장 좋아하는 방법을 선택하겠습니다:
1
에서100
까지 반복합니다.101
의 범위를 사용하세요.- 각 숫자에 대해
3
과5
로 나눈 나머지(모듈러스)를 계산합니다. - 나머지가
0
이면 해당 수는 주어진 인수의 배수입니다.- 나머지가
0
이15
의 배수인 경우,FizzBuzz
를 출력합니다. - 나머지가
0
이3
의 배수인 경우,Fizz
를 출력합니다. - 나머지가
0
이5
의 배수인 경우,Buzz
를 출력합니다.
- 나머지가
- 그렇지 않으면 숫자 자체를 출력합니다.
단계별 따라하기
이 단계별 튜토리얼을 따라하기 위해서는 Python 설치와 pipenv
설치가 필요합니다.
🐰 이 게시물의 소스 코드는 GitHub에서 제공됩니다.
game.py
라는 이름의 Python 파일을 만들고,
위의 FizzBuzz 구현을 그 내용으로 설정하십시오.
그러고 나서 python game.py
를 실행하십시오.
출력은 다음과 같아야 합니다:
🐰 붐! 당신은 코딩 인터뷰를 해치웠습니다!
더 진행하기 전에, 마이크로 벤치마킹과 매크로 벤치마킹 사이의 차이점을 논의하는 것이 중요합니다.
마이크로-벤치마킹 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 로직을 몇 가지 함수로 리팩토링해 봅시다:
play_game
: 정수n
을 입력으로 받아fizz_buzz
를 그 숫자로 호출하며,should_print
가True
일 경우 결과를 출력합니다.fizz_buzz
: 정수n
을 입력으로 받아 실제Fizz
,Buzz
,FizzBuzz
또는 숫자 로직을 수행하고 결과를 문자열로 반환합니다.
그런 다음 메인 실행을 다음과 같이 업데이트합니다:
우리 프로그램의 메인 실행은 1
부터 100
까지의 숫자를 포함하여 반복하며, 각 숫자에 대해 should_print
가 True
로 설정된 상태로 play_game
을 호출합니다.
피즈버즈 벤치마킹
코드를 벤치마크하기 위해, 벤치마크를 실행하는 테스트 함수를 생성해야 합니다.
game.py
의 맨 아래에 다음 코드를 추가하세요:
pytest-benchmark
의benchmark
픽스처를 받는test_game
이라는 이름의 함수를 생성합니다.1
부터100
까지 포괄적으로 반복하는run_game
함수를 생성합니다.- 각 숫자에 대해,
should_print
을False
로 설정하여play_game
을 호출합니다.
- 각 숫자에 대해,
benchmark
러너에run_game
함수를 전달합니다.
이제 벤치마크를 실행하기 위해 프로젝트를 설정해야 합니다.
pipenv
로 새로운 가상 환경을 생성하세요:
새로운 pipenv
환경 내에 pytest-benchmark
를 설치하세요:
이제 코드를 벤치마크할 준비가 되었습니다. pytest game.py
를 실행하세요:
🐰 양배추가 많이 들어간 비트를 즐기세요! 첫 벤치마크 메트릭이 나왔습니다!
마지막으로, 피곤한 개발자의 머리를 쉬게 할 수 있습니다… 농담입니다. 사용자들은 새로운 기능을 원합니다!
파이썬으로 FizzBuzzFibonacci 작성하기
우리의 핵심 성과 지표(KPIs)가 떨어졌습니다. 그래서 제품 관리자(PM)는 새로운 기능을 추가하기를 원합니다. 많은 브레인스토밍과 사용자 인터뷰 후에, 전통적인 FizzBuzz만으로는 충분하지 않다는 결론에 도달했습니다. 요즘 아이들은 새로운 게임인 FizzBuzzFibonacci를 원합니다.
FizzBuzzFibonacci의 규칙은 다음과 같습니다:
1
에서100
까지의 정수를 출력하는 프로그램을 작성하세요 :
- 3의 배수는
Fizz
를 출력합니다.- 5의 배수는
Buzz
를 출력합니다.- 3과 5의 배수 모두인 경우에는
FizzBuzz
를 출력합니다.- 피보나치 수열의 일부인 숫자는
Fibonacci
만 출력합니다.- 그 외의 모든 숫자는 숫자를 출력합니다.
피보나치 수열은 각 숫자가 그 이전 두 숫자의 합계인 수열입니다.
예를 들어, 0
과 1
에서 시작하여 피보나치 수열의 다음 숫자는 1
입니다.
그 다음에는 2
, 3
, 5
, 8
등이 이어집니다.
피보나치 수열의 일부인 숫자를 피보나치 수라고 합니다. 따라서 우리는 피보나치 수를 감지하는 함수를 작성해야 합니다.
피보나치 수열을 작성하는 방법은 많이 있습니다 마찬가지로 피보나치 수를 감지하는 방법도 많습니다. 따라서 제가 가장 선호하는 방법을 선택하겠습니다:
- 정수를 입력받아 부울 값을 반환하는
is_fibonacci_number
라는 함수를 만듭니다. - 주어진 숫자
n
까지 숫자0
에서부터 반복합니다. 이전
숫자와현재
숫자로 각각0
과1
로 시작하는 피보나치 수열을 초기화합니다.현재
숫자가 현재 반복i
보다 작을 동안 반복합니다.이전
숫자와현재
숫자를 더해next_value
숫자를 얻습니다.이전
숫자를현재
숫자로 업데이트합니다.현재
숫자를next_value
숫자로 업데이트합니다.현재
가 주어진 숫자n
보다 크거나 같아질 때 루프를 종료합니다.현재
숫자가 주어진 숫자n
과 같은지 확인하고 만약 그렇다면True
를 반환합니다.- 그렇지 않으면
False
를 반환합니다.
이제 fizz_buzz
함수를 업데이트해야 합니다:
fizz_buzz
함수를 보다 설명적인 이름인fizz_buzz_fibonacci
로 변경합니다.is_fibonacci_number
헬퍼 함수를 호출합니다.is_fibonacci_number
의 결과가True
이면Fibonacci
를 반환합니다.is_fibonacci_number
의 결과가False
이면 동일한Fizz
,Buzz
,FizzBuzz
또는 숫자 로직을 수행하여 결과를 반환합니다.
fizz_buzz
를 fizz_buzz_fibonacci
로 이름을 변경했기 때문에 play_game
함수도 업데이트해야 합니다:
주요 실행과 test_game
함수 둘 다 정확히 동일하게 유지할 수 있습니다.
피즈버즈피보나치 벤치마크
이제 벤치마크를 다시 실행할 수 있습니다:
터미널 기록을 다시 스크롤하여 피즈버즈와 피즈버즈피보나치 게임의 성능을 대략 비교할 수 있습니다: 10.8307 us
vs 735.5682 us
.
여러분의 숫자는 제 것과 약간 다를 수 있습니다.
하지만 두 게임 간의 차이는 아마도 50배 정도일 것입니다.
게임에 _피보나치_라는 멋지게 들리는 기능을 추가하기 위해서는 그 정도면 좋다고 생각합니다.
아이들이 좋아할 것입니다!
Python에서 FizzBuzzFibonacci 확대하기
우리의 게임이 대박을 쳤습니다! 아이들이 정말로 FizzBuzzFibonacci 게임을 즐겨합니다.
그렇게 해서 경영진으로부터 후속작을 만들라는 요청이 내려왔습니다.
하지만 현대 사회에서는, 일회성 구매가 아닌 연간 경상 수익(ARR)이 필요합니다!
우리 게임의 새로운 비전은 개방형 게임이라는 것으로, 더 이상 1
과 100
사이(포함)에서 머무르지 않습니다.
아니요, 우리는 새로운 경계를 향해 나아갑니다!
Open World FizzBuzzFibonacci의 규칙은 다음과 같습니다:
양의 정수를 입력으로 받아 다음을 출력하는 프로그램을 작성하십시오:
- 세의 배수인 경우
Fizz
를 출력합니다- 다섯의 배수인 경우
Buzz
를 출력합니다- 세와 다섯의 공배수인 경우
FizzBuzz
를 출력합니다- 피보나치 수열에 포함되는 숫자인 경우
Fibonacci
만 출력합니다- 그 외의 숫자는 숫자 그대로 출력합니다.
우리 게임이 어떤 숫자에서든 작동하려면 명령줄 인수를 받아야 합니다. 주 실행을 다음과 같이 업데이트하세요:
sys
패키지를 가져옵니다.- 명령줄에서 우리 게임으로 전달된 모든 인수(
args
)를 수집합니다. - 우리 게임에 전달된 첫 번째 인수를 가져와서 숫자인지 확인합니다.
- 숫자라면, 첫 번째 인수를 정수
i
로 파싱합니다. - 새로 파싱된 정수
i
로 우리의 게임을 실행합니다.
- 숫자라면, 첫 번째 인수를 정수
- 파싱에 실패하거나 어떤 인수도 전달되지 않는다면, 유효한 입력을 요청하는 기본값으로 설정합니다.
이제 어떤 숫자로든 우리의 게임을 플레이할 수 있습니다!
게임을 하려면 python game.py
명령 뒤에 정수를 입력하세요:
그리고 잘못된 수를 생략하거나 제공하는 경우:
와우, 그것은 진정한 철저한 테스트였습니다! CI가 통과했습니다. 상사들도 기뻐합니다. 배포합시다! 🚀
끝
🐰 … 아마 당신의 경력의 끝일까요?
농담이었어요! 모든 것이 불타고 있어요! 🔥
처음에는 모든 것이 잘 진행된 것처럼 보였어요. 그런데 토요일 새벽 2시 7분에 내 호출기가 울렸습니다:
📟 당신의 게임이 불타고 있어요! 🔥
침대에서 뛰어나와서 무슨 일이 일어나고 있는지 알아내려고 노력했습니다. 로그를 검색하려고 했지만, 모든 것이 계속 충돌해서 검색이 어려웠습니다. 드디어 문제를 찾았습니다. 아이들이었습니다. 아이들이 우리 게임을 너무 좋아해서 백만 번이나 플레이하고 있었습니다! 기백한 두뇌를 깨우고, 두 개의 새로운 벤치마크를 추가했습니다:
- 숫자 백(
100
)을 사용하여 게임을 실행하는 마이크로-벤치마크test_game_100
- 숫자 백만(
1_000_000
)을 사용하여 게임을 실행하는 마이크로-벤치마크test_game_1_000_000
제가 실행했을 때, 이런 결과가 나왔습니다:
잠깐만 기다려 보세요…
뭐라고요! 15.8470 us
x 1,000
이면 15,847.0 us
이 되어야 하는데 571,684.6334 us
라니 🤯
비록 피보나치 수열 코드가 기능적으로는 올바르게 작동했지만, 어딘가에 성능 버그가 있는 것이 틀림없습니다.
Python에서 FizzBuzzFibonacci 수정하기
is_fibonacci_number
함수를 다시 한 번 살펴봅시다:
이제 성능에 대해 생각해보니, 불필요한 추가 루프가 있다는 것을 깨달았습니다. for i in range(n + 1):
루프를 완전히 제거하고 주어진 숫자(n
)와 current
값을 비교할 수 있습니다. 🤦
is_fibonacci_number
함수를 업데이트합니다.- 피보나치 수열을
previous
와current
숫자로 각각0
과1
로 시작합니다. current
숫자가 주어진 숫자n
보다 작은 동안 반복합니다.previous
와current
숫자를 더하여next_value
숫자를 얻습니다.previous
숫자를current
숫자로 업데이트합니다.current
숫자를next_value
숫자로 업데이트합니다.current
가 주어진 숫자n
보다 크거나 같아지면 루프를 종료합니다.current
숫자가 주어진 숫자n
과 같은지 확인하고 그 결과를 반환합니다.
이제 벤치마크를 다시 실행하여 어떻게 되었는지 확인해 봅시다:
오, 세상에! 우리의 test_game
벤치마크가 원래의 FizzBuzz에 근접한 수준으로 다시 내려왔습니다.
그 점수가 정확히 무엇이었는지 기억할 수 있었으면 좋겠네요. 벌써 3주나 지났네요.
터미널 히스토리는 그렇게 오래가지 않거든요.
그리고 pytest-benchmark
는 우리가 요청해야만 결과를 저장합니다.
하지만 거의 근접했다고 생각합니다!
test_game_100
벤치마크는 거의 50배나 내려가서 322.0815 ns
가 되었습니다.
그리고 test_game_1_000_000
벤치마크는 500,000배 이상 내려갔습니다! 571,684,633.4 ns
가 753.1445 ns
로!
🐰 적어도 이 성능 버그가 프로덕션에 배포되기 전에 발견했다는 사실에 감사해야겠죠… 아, 맞다. 그냥 잊으세요…
CI에서 성능 저하를 포착하세요
나의 작은 성능 버그로 인해 우리 게임이 받은 부정적인 리뷰들에 대해 경영진들은 만족하지 않았습니다. 그들은 저에게 다시는 그런 일이 발생하지 않도록 하라고 말했고, 어떻게 해야 하는지 물었을 때, 그들은 그냥 다시는 하지 말라고만 말했습니다. 어떻게 그렇게 관리해야 하지요‽
다행스럽게도, 저는 Bencher라는 놀라운 오픈 소스 툴을 발견했습니다. 아주 관대한 무료 티어가 있으므로 저는 개인 프로젝트에 Bencher Cloud를 그냥 사용할 수 있습니다. 그리고 회사에서는 모든 것이 우리의 프라이빗 클라우드에 있어야 하므로, 저는 Bencher Self-Hosted를 사용하기 시작했습니다.
Bencher는 내장 어댑터가 있으므로 CI에 통합하기 쉽습니다. 빠른 시작 가이드를 따른 후, Bencher를 이용해 벤치마크를 실행하고 추적할 수 있습니다.
이 멋진 시간 여행 장치를 통해, 친절한 토끼가 내게 준 것을 활용하여, Bencher를 계속 사용했더라면 무슨 일이 일어났을지 되돌려보았습니다. 처음으로 버그가 있는 FizzBuzzFibonacci 구현을 푸시한 곳을 볼 수 있습니다. 나는 즉시 내 풀 요청에 대해 CI에서 실패를 받았습니다. 그날 저녁, 저는 그 불필요하고, 추가적인 반복문을 제거함으로써 성능 버그를 수정했습니다. 화재는 없습니다. 그저 행복한 사용자들만 있습니다.
Bencher: 지속적인 벤치마킹
Bencher는 지속적인 벤치마킹 도구 모음입니다. 성능 회귀가 사용자에게 영향을 미친 경험이 있나요? Bencher가 그런 일이 일어나는 것을 막을 수 있었습니다. Bencher를 이용하면 성능 회귀를 상용 환경으로 이동하기 전에 탐지하고 예방할 수 있습니다.
- 실행: 기존 벤치마킹 도구를 사용하여 로컬 또는 CI에서 벤치마크를 실행합니다.
bencher
CLI는 기존 벤치마킹 하네스를 감싸고 결과를 저장합니다. - 추적: 벤치마크 결과를 시간이 지남에 따라 추적합니다. 소스 브랜치, 테스트 베드, 측정 기반의 Bencher 웹 콘솔을 사용하여 결과를 모니터링, 쿼리, 그래프로 만듭니다.
- 캐치: CI에서 성능 회귀를 잡아냅니다. Bencher는 최첨단, 사용자 정의 가능한 분석을 사용하여 상용 환경으로 가기 전에 성능 회귀를 탐지합니다.
단위 테스트가 CI에서 기능 회귀를 방지하기 위해 실행되는 것처럼, 벤치마크는 Bencher와 함께 CI에서 실행되어 성능 회귀를 방지해야 합니다. 성능 버그도 버그입니다!
CI에서 성능 회귀를 잡아내기 시작하세요 - Bencher Cloud를 무료로 시도해보세요.