Rust 코드를 Iai로 벤치마킹하는 방법
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가 지원합니다. 그렇다면 왜 Iai를 선택하나요? Iai는 벽시계 시간 대신 명령 수를 사용합니다. 이는 CI에서의 지속적인 벤치마킹에 이상적입니다. 특히 공유 실행자를 사용하는 경우에는 Iai를 사용하여 지속적인 벤치마킹을 하는 것이 좋습니다. Iai가 실제로 중요한 것에 대한 프록시만 측정한다는 것을 이해하는 것이 중요합니다. 1,000개의 명령이 2,000개로 늘어나면 애플리케이션의 대기 시간이 두 배가 됩니까? 아마 그렇거나 그렇지 않을 수 있습니다. 이런 이유로, 명령 개수에 기반을 둔 벤치마크와 동시에 벽시계 시간에 기반을 둔 벤치마크를 실행하는 것이 유용할 수 있습니다.
🐰 Iai는 3년 이상 업데이트되지 않았습니다. 그래서 Iai-Callgrind를 사용하는 것을 고려해 볼 수 있습니다.
Valgrind 설치
Iai는 명령 수를 수집하기 위해 Valgrind라는 도구를 사용합니다. Valgrind는 Linux, Solaris, FreeBSD, 그리고 MacOS를 지원합니다. 그러나 MacOS 지원은 아직 지원되지 않는 arm64 (M1, M2 등) 프로세서를 제외한 x86_64 프로세서로 제한됩니다.
Debian에서 실행: sudo apt-get install valgrind
MacOS (x86_64/Intel chip only): brew install valgrind
FizzBuzz 재구성
FizzBuzz 애플리케이션을 테스트하기 위해서는 우리의 로직을 프로그램의 main
함수에서 분리해야 합니다.
벤치마크 하니스는 main
함수를 벤치마크할 수 없습니다. 이를 위해서는 몇 가지 변경이 필요합니다.
src
내에 lib.rs
라는 새 파일을 생성하세요:
다음의 코드를 lib.rs
에 추가하세요:
play_game
: 부호 없는 정수n
을 인자로 받아,fizz_buzz
를 해당 숫자와 함께 호출하고,print
가true
일 경우 결과를 출력합니다.fizz_buzz
: 부호 없는 정수n
을 인자로 받아 실제Fizz
,Buzz
,FizzBuzz
, 또는 숫자 로직을 수행하고 결과를 문자열로 반환합니다.
그리고 main.rs
를 다음과 같이 업데이트하세요:
game::play_game
: 우리가lib.rs
로 생성한game
크레이트에서play_game
를 가져옵니다.main
: 프로그램의 주 진입점으로, 숫자1
에서100
까지 반복하면서 각 숫자에 대해print
를true
로 설정한 상태로play_game
를 호출합니다.
FizzBuzz 벤치마킹
코드를 벤치마크하려면 benches
디렉토리를 생성하고 벤치마크를 포함할 파일인 play_game.rs
를 추가해야 합니다:
play_game.rs
내에 다음 코드를 추가합니다:
game
크레이트에서play_game
함수를 import합니다.bench_play_game
이라는 함수를 생성합니다.- 컴파일러가 우리의 코드를 최적화하지 못하도록 “black box” 내에서 우리의 macro-benchmark를 실행합니다.
1
부터100
까지 순차적으로 반복합니다.- 각 숫자에 대해
play_game
을 호출하고,print
를false
로 설정합니다.
이제 벤치마크를 실행하기 위해 game
크레이트를 설정해야 합니다.
Cargo.toml
파일의 _바닥_에 다음을 추가합니다:
iai
:iai
를 개발 종속성으로 추가합니다. 왜냐하면 우리는 이를 성능 테스팅을 위해 사용하기 때문입니다.bench
:play_game
을 벤치마크로 등록하고harness
를false
로 설정합니다. 왜냐하면 벤치마킹 하네스로 Iai를 사용할 것이기 때문입니다.
이제 코드를 벤치마킹할 준비가 되었습니다, 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 벤치마킹
이제 벤치마크를 다시 실행할 수 있습니다:
오, 깔끔! Iai는 우리의 FizzBuzz와 FizzBuzzFibonacci 게임의 추정된 사이클 사이의 차이는 +522.6091%
라고 알려줍니다.
당신의 숫자는 제 숫자와 약간 다를 것입니다.
그러나 두 게임 사이의 차이는 아마 5x
범위에 있을 겁니다.
그게 좋은 것 같아요! 특히 우리 게임에 _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
그것을 실행했을 때, 저는 이렇게 받았습니다:
기다리고… 기다리고…
무엇! 6,685 추정 사이클
x 1,000
은 6,685,000 추정 사이클
이어야 하지 155,109,206 추정 사이클
이 아니야 🤯
비록 나의 피보나치 수열 코드가 기능적으로 정확하다고 해도, 어딘가에 성능 버그가 있어야 합니다.
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에서처럼 다시 떨어졌네요.
그 점수가 정확히 얼마나 됐었는지 기억하고 싶군요. 이제 세 주가 지났네요.
내 터미널 기록은 그렇게 멀리 돌아가지 않아요.
그리고 Iai는 가장 최근의 결과와만 비교합니다.
하지만 가까워 보이네요!
bench_play_game_100
벤치마크는 거의 10배 떨어졌고, -87.22513%
.
그리고 bench_play_game_1_000_000
벤치마크는 10,000배 이상 떨어졌습니다! 155,109,206 추정 사이클
에서 950
추정 사이클로!
-99.99939%
!
🐰 헤이, 적어도 이 성능 버그를 프로덕션에 들어가기 전에 잡았다… 아, 맞다. 실수했다…
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를 무료로 시도해보세요.