libtest 벤치를 사용하여 Rust 코드 벤치마킹하기
Everett Pompeii
벤치마킹이란 무엇인가요?
벤치마킹은 코드의 성능을 테스트하여 코드가 얼마나 빠르게(지연시간) 또는 얼마나 많은 양(처리량)의 작업을 수행할 수 있는지를 확인하는 실습입니다. 이는 소프트웨어 개발에서 종종 간과되지만, 빠르고 성능 좋은 코드를 작성하고 유지하기 위해 매우 중요한 단계입니다. 벤치마킹은 개발자가 다양한 작업 부하와 조건에서 코드가 얼마나 잘 수행되는지를 이해하는 데 필요한 메트릭을 제공합니다. 기능 회귀를 방지하기 위해 단위 테스트와 통합 테스트를 작성하는 것과 같은 이유로, 성능 회귀를 방지하기 위해 벤치마크를 작성해야 합니다. 성능 버그도 버그입니다!
Rust에서 FizzBuzz 작성하기
벤치마크를 작성하기 위해서는 벤치마크할 소스 코드가 필요합니다. 처음으로 매우 간단한 프로그램인, FizzBuzz를 작성해 봅시다.
FizzBuzz의 규칙은 다음과 같습니다:
1
부터100
까지의 정수를 출력하는 프로그램을 작성하십시오:
- 3의 배수인 경우
Fizz
를 출력합니다- 5의 배수인 경우
Buzz
를 출력합니다- 3과 5의 공배수인 경우
FizzBuzz
를 출력합니다- 그 외의 경우에는 숫자를 출력합니다.
FizzBuzz를 작성하는 방법은 여러 가지가 있습니다. 그래서 저는 제가 가장 좋아하는 방법을 선택하겠습니다:
main
함수를 생성하십시오.1
에서100
까지 포함하여 반복하세요.- 각 숫자에 대해
3
과5
모두에 대한 나머지(나눗셈 후 나머지)를 계산하십시오. - 두 나머지에 대해 패턴 매치를 합니다.
나머지가
0
이면 해당 숫자는 주어진 인수의 배수입니다. 3
과5
모두의 나머지가0
이면FizzBuzz
를 출력하십시오.3
의 나머지만0
이면Fizz
를 출력하십시오.5
의 나머지만0
이면Buzz
를 출력하십시오.- 그렇지 않으면 숫자를 그대로 출력하십시오.
단계별 따라하기
이 단계별 튜토리얼을 따라가려면, 먼저 Rust를 설치해야 합니다.
🐰 이 게시물의 소스 코드는 GitHub에서 사용할 수 있습니다
Rust를 설치한 후, 터미널 창을 열고 cargo init game
을 입력할 수 있습니다.
그런 다음 새로 만들어진 game
디렉토리로 이동합니다.
src
라는 디렉토리와 main.rs
라는 파일을 볼 수 있어야 합니다:
그 내용을 위의 FizzBuzz 구현으로 교체한 다음 cargo run
를 실행합니다.
출력 결과는 다음과 같아야 합니다:
🐰 펑! 코딩 인터뷰를 깨고 있습니다!
새로운 Cargo.lock
파일이 생성되어야 합니다:
더 진행하기 전에, 미세 벤치마킹과 거시 벤치마킹의 차이점을 논의하는 것이 중요합니다.
마이크로-벤치마킹 vs 매크로-벤치마킹
소프트웨어 벤치마크의 두 가지 주요 카테고리는 마이크로-벤치마크와 매크로-벤치마크입니다.
마이크로-벤치마크는 단위 테스트와 유사한 수준에서 작동합니다.
예를 들어, 단일 숫자에 대해 Fizz
, Buzz
, 또는 FizzBuzz
를 결정하는 함수에 대한 벤치마크는 마이크로-벤치마크가 될 것입니다.
매크로-벤치마크는 통합 테스트와 유사한 수준에서 작동합니다.
예를 들어, 1
에서 100
까지의 FizzBuzz 게임을 전체적으로 실행하는 함수의 벤치마크는 매크로-벤치마크가 될 것입니다.
일반적으로 가능한 최저 수준의 추상화에서 테스트하는 것이 가장 좋습니다. 벤치마크의 경우, 이를 유지 관리하기 쉽게 만들고, 측정에서의 잡음을 줄이는 데 도움이 됩니다. 그러나, 몇 가지 종단 간 테스트가 전체 시스템이 예상대로 잘 조합되는지 정상 체크에 매우 유용하게 작용하는 것처럼, 매크로-벤치마크가 있으면 소프트웨어를 통한 중요한 경로가 성능을 유지하는지 확인하는데 매우 유용할 수 있습니다.
러스트에서의 벤치마킹
러스트에서 벤치마킹을 위한 세 가지 인기 있는 옵션은 다음과 같습니다: libtest bench, Criterion, 그리고 Iai.
libtest는 러스트의 내장 단위 테스트 및 벤치마킹 프레임워크입니다.
러스트 표준 라이브러리의 일부이지만, libtest 벤치는 여전히 불안정한 상태로 간주되어
nightly
컴파일러 릴리스에서만 사용할 수 있습니다.
안정적인 러스트 컴파일러에서 작업하려면,
별도의 벤치마킹 하네스를 사용해야 합니다.
하지만 그 어느 것도 활발히 개발되고 있지는 않습니다.
러스트 생태계 내에서 가장 인기 있는 벤치마킹 하네스는 Criterion입니다.
이것은 안정적인 컴파일러와 nightly
컴파일러에서 모두 작동하며,
러스트 커뮤니티 내에서 사실상의 표준이 되었습니다.
Criterion은 libtest 벤치에 비해 훨씬 더 많은 기능을 제공합니다.
Criterion의 실험적 대안은 같은 제작자로부터 나온 Iai입니다. 그러나 이는 월드 시계 시간을 대신하여 명령어 수를 사용합니다: CPU 명령어, L1 접근, L2 접근 및 RAM 접근. 이로 인해 이러한 메트릭은 실행 간에 거의 동일하게 유지되므로 단일 샷 벤치마킹이 가능합니다.
이 세 가지는 모두 Bencher가 지원합니다. 그렇다면 왜 libtest bench를 선택해야 할까요?
프로젝트의 외부 종속성을 제한하려고 시도하고 프로젝트가 이미 nightly
툴체인을 사용하고 있는 경우 좋은 선택이 될 수 있습니다.
그 외에는 사용 사례에 따라 Criterion 또는 Iai를 사용하는 것을 권장합니다.
Rust nightly
설치
이제 준비가 되었으니, libtest bench를 사용할 것이므로 우리의 Rust 툴체인을 nightly
로 설정합시다.
game
프로젝트의 루트에 Cargo.toml
옆에 rust-toolchain.toml
파일을 만듭니다.
이제 디렉토리 구조는 다음과 같아야 합니다:
완료되었다면, 다시 cargo run
을 실행합니다.
새롭게, nightly
툴체인이 설치되기까지 조금 시간이 걸릴 것입니다
이전과 같은 출력을 제공하기 전에 다시 실행합니다.
FizzBuzz Refactor
FizzBuzz 애플리케이션을 테스트하기 위해서는 로직을 프로그램의 main
함수에서 분리해야 합니다.
벤치마크 하네스는 main
함수를 벤치마크할 수 없습니다.
코드를 다음과 같이 업데이트 합니다:
우리는 이제 코드를 아래와 같이 세 가지 다른 함수로 분류했습니다:
main
: 프로그램의 주요 진입점으로 숫자1
에서100
까지 포함하여 반복하고 각 숫자에 대해play_game
을 호출합니다.play_game
: 부호 없는 정수n
을 입력으로 받고, 그 숫자를 사용하여fizz_buzz
를 호출하고 결과를 출력합니다.fizz_buzz
: 부호 없는 정수n
을 인자로 받고 실제로Fizz
,Buzz
,FizzBuzz
, 또는 숫자 로직을 수행하여 결과를 문자열로 반환합니다.
FizzBuzz 벤치마킹
불안정한 libtest crate를 사용하려면 코드에 test
기능을 활성화하고 test
crate를 가져와야 합니다. 다음을 main.rs
의 _맨 위_에 추가합니다:
이제 첫 번째 벤치마크를 추가할 준비가 되었습니다!
다음을 main.rs
의 _맨 아래_에 추가합니다:
- ‘benchmarks’라는 모듈을 만들고
컴파일러 설정
을
test
mode로 설정합니다. - 벤치마크 러너
Bencher
를 가져옵니다. (🐰 멋진 이름이네요!) - 우리의
play_game
함수를 가져옵니다. Bencher
에 대한 변경 가능한 참조를 가져오는bench_play_game
이라는 벤치마크를 만듭니다.#[bench]
특성을 설정하여bench_play_game
이 벤치마크임을 나타냅니다.Bencher
인스턴스 (b
)를 사용하여 우리의 매크로 벤치마크를 여러 번 실행합니다.- 컴파일러가 우리의 코드를 최적화하지 않도록 “블랙 박스” 내에서 우리의 매크로 벤치마크를 실행합니다.
1
에서100
까지 포함하여 반복합니다.- 각 번호에 대해
play_game
을 호출합니다.
이제 코드 벤치마킹을 준비했으니, cargo bench
를 실행합니다:
🐰 Lettuce turnip the beet! 우리는 첫 번째 벤치마크 메트릭을 얻었습니다!
드디어 우리는 피곤한 개발자 머리를 쉴 수 있습니다… 그냥 장난이에요, 사용자들이 새로운 기능을 원합니다!
Rust로 FizzBuzzFibonacci 쓰기
우리의 주요 성과 지표 (KPI)가 하락하여 상품 관리자 (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
부터 모든 숫자에 대해 반복합니다. previous
와current
숫자를 각각0
과1
로 시작하는 피보나치 수열을 초기화합니다.current
숫자가 현재 반복i
보다 작은 동안 반복합니다.previous
와current
숫자를 더하여next
숫자를 가져옵니다.previous
숫자를current
숫자로 업데이트합니다.current
숫자를next
숫자로 업데이트합니다.current
가 주어진 숫자n
보다 크거나 같으면 루프를 종료합니다.current
숫자가 주어진 숫자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
함수도 업데이트해야 합니다:
우리의 main
함수와 bench_play_game
함수는 그대로 유지될 수 있습니다.
FizzBuzzFibonacci 벤치마킹
이제 벤치마크를 다시 실행할 수 있습니다:
터미널 기록을 뒤로 스크롤하여,
우리의 FizzBuzz와 FizzBuzzFibonacci 게임의 성능 사이의 차이를 눈으로 비교할 수 있습니다: 4,879 ns
대 22,167 ns
.
당신의 숫자는 나의 것과 약간 다를 것입니다.
그러나 두 게임 사이의 차이는 아마도 5배 범위일 것입니다.
그것은 나에게 좋아 보입니다! 특히 _Fibonacci_와 같은 멋진 소리하는 기능을 우리 게임에 추가하는 것에 대해.
아이들이 그것을 사랑할 것입니다!
Rust에서 FizzBuzzFibonacci 확장하기
우리의 게임이 대박입니다! 어린이들이 FizzBuzzFibonacci를 정말 좋아하고 있어요.
그만큼 인기가 많아 익사들로 부터 후속작을 만들라는 소식이 전해졌습니다.
하지만 이건 현대적인 세계, 단번에 구매를 하는 것이 아닌 연간 재발생 수익(ARR)이 필요합니다!
우리 게임의 새로운 비전은 끝이 없는 것입니다, 1
과 100
사이의 한계에 묶여 사는 것은 없습니다 (비록 포함된다 해도).
아니요, 우리는 새로운 경계를 넘어가고 있습니다!
Open World FizzBuzzFibonacci의 규칙은 다음과 같습니다:
양의 정수를 입력으로 받아 다음을 출력하는 프로그램을 작성하십시오:
- 세의 배수인 경우
Fizz
를 출력합니다- 다섯의 배수인 경우
Buzz
를 출력합니다- 세와 다섯의 공배수인 경우
FizzBuzz
를 출력합니다- 피보나치 수열에 포함되는 숫자인 경우
Fibonacci
만 출력합니다- 그 외의 숫자는 숫자 그대로 출력합니다.
우리의 게임이 어떤 숫자든 작동하도록 하기 위해, 우리는 명령행 인자를 받아들일 필요가 있습니다.
main
함수를 다음과 같이 업데이트합니다:
- 명령행에서 우리 게임에 전달된 모든 인자 (
args
)를 수집합니다. - 우리 게임에 전달된 첫 번째 인자를 부호 없는 정수
i
로 파싱합니다. - 파싱이 실패하거나 인자가 전달되지 않으면, 우리 게임을 입력값으로
15
를 사용하여 기본으로 실행합니다. - 마지막으로, 새로 파싱된 부호 없는 정수
i
로 우리 게임을 실행합니다.
이제 우리는 어떤 숫자로든 우리의 게임을 할 수 있습니다!
명령행 인자를 우리 게임에 전달하려면 --
를 따라 cargo run
을 사용하세요:
그리고 우리가 숫자를 생략하거나 유효하지 않은 숫자를 제공하면:
와우, 그것은 상당히 철저한 테스팅 이었습니다! CI가 통과합니다. 우리의 상사들은 매우 만족합니다. 발송해봅시다! 🚀
끝
🐰 … 아마 당신의 경력의 끝일까요?
농담이었어요! 모든 것이 불타고 있어요! 🔥
처음에는 모든 것이 잘 진행된 것처럼 보였어요. 그런데 토요일 새벽 2시 7분에 내 호출기가 울렸습니다:
📟 당신의 게임이 불타고 있어요! 🔥
침대에서 뛰어나와서 무슨 일이 일어나고 있는지 알아내려고 노력했습니다. 로그를 검색하려고 했지만, 모든 것이 계속 충돌해서 검색이 어려웠습니다. 드디어 문제를 찾았습니다. 아이들이었습니다. 아이들이 우리 게임을 너무 좋아해서 백만 번이나 플레이하고 있었습니다! 기백한 두뇌를 깨우고, 두 개의 새로운 벤치마크를 추가했습니다:
- 숫자 한 백 (
100
)으로 게임을 하는 마이크로 벤치마크bench_play_game_100
- 숫자 백만 (
1_000_000
)으로 게임을 하는 마이크로 벤치마크bench_play_game_1_000_000
내가 실행하면 이렇게 나옵니다:
기다려봅시다… 기다려봅시다…
뭐야! 439 ns
x 1,000
은 439,000 ns
이야, 9,586,977 ns
가 아니야 🤯
Fibonacci sequence 코드가 기능적으로는 잘 돌아가지만, 어딘가에 성능 버그가 있어야 합니다.
Rust에서 FizzBuzzFibonacci 수정하기
is_fibonacci_number
함수를 다시 한번 살펴보겠습니다:
성능에 대해 생각해보니, 불필요하고 여분의 루프를 가지고 있다는 것을 깨닫게 되었습니다.
for i in 0..=n {}
루프를 완전히 제거하고
주어진 수(n
)와 current
값을 그냥 비교할 수 있습니다 🤦
is_fibonacci_number
함수를 업데이트하세요.- 피보나치 수열을
previous
와current
수를 각각0
과1
로 시작하여 초기화하세요. - 주어진 수
n
보다current
숫자가 작은 동안 반복하세요. previous
와current
숫자를 더하여next
숫자를 얻으십시오.previous
숫자를current
숫자로 업데이트합니다.current
숫자를next
숫자로 업데이트합니다.current
가 주어진 번호n
보다 크거나 같으면 루프를 종료합니다.current
숫자가 주어진 번호n
과 같은지 확인하고 그 결과를 반환합니다.
이제 벤치마크를 다시 실행하고 어떻게 했는지 봅시다:
오, 와! 우리의 bench_play_game
벤치마크가 원래 FizzBuzz의 점수 주변으로 다시 내려왔습니다.
내가 그 점수가 정확히 얼마였는지 기억할 수 있었으면 좋겠어. 그러나 이미 3주가 지났어.
내 터미널 기록은 그렇게 멀리 돌아가지 않아.
하지만 가깝다고 생각합니다!
bench_play_game_100
벤치마크는 거의 10배 가량 내려갔습니다. 439 ns
에서 46 ns
로.
그리고 bench_play_game_1_000_000
벤치마크는 10,000배 이상 내려갔습니다! 9,586,977 ns
에서 53 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를 무료로 시도해보세요.