Gungraun으로 Rust 코드를 벤치마크하는 방법
Everett Pompeii
벤치마킹이란 무엇인가요?
벤치마킹은 코드의 성능을 테스트하여 코드가 얼마나 빠르게(지연시간) 또는 얼마나 많은 양(처리량)의 작업을 수행할 수 있는지를 확인하는 실습입니다. 이는 소프트웨어 개발에서 종종 간과되지만, 빠르고 성능 좋은 코드를 작성하고 유지하기 위해 매우 중요한 단계입니다. 벤치마킹은 개발자가 다양한 작업 부하와 조건에서 코드가 얼마나 잘 수행되는지를 이해하는 데 필요한 메트릭을 제공합니다. 기능 회귀를 방지하기 위해 단위 테스트와 통합 테스트를 작성하는 것과 같은 이유로, 성능 회귀를 방지하기 위해 벤치마크를 작성해야 합니다. 성능 버그도 버그입니다!
Rust에서 FizzBuzz 작성하기
벤치마크를 작성하기 위해서는 벤치마크할 소스 코드가 필요합니다. 처음으로 매우 간단한 프로그램인, FizzBuzz를 작성해 봅시다.
FizzBuzz의 규칙은 다음과 같습니다:
1부터100까지의 정수를 출력하는 프로그램을 작성하십시오:
- 3의 배수인 경우
Fizz를 출력합니다- 5의 배수인 경우
Buzz를 출력합니다- 3과 5의 공배수인 경우
FizzBuzz를 출력합니다- 그 외의 경우에는 숫자를 출력합니다.
FizzBuzz를 작성하는 방법은 여러 가지가 있습니다. 그래서 저는 제가 가장 좋아하는 방법을 선택하겠습니다:
fn main() { for i in 1..=100 { match (i % 3, i % 5) { (0, 0) => println!("FizzBuzz"), (0, _) => println!("Fizz"), (_, 0) => println!("Buzz"), (_, _) => println!("{i}"), } }}main함수를 생성하십시오.1에서100까지 포함하여 반복하세요.- 각 숫자에 대해
3과5모두에 대한 나머지(나눗셈 후 나머지)를 계산하십시오. - 두 나머지에 대해 패턴 매치를 합니다.
나머지가
0이면 해당 숫자는 주어진 인수의 배수입니다. 3과5모두의 나머지가0이면FizzBuzz를 출력하십시오.3의 나머지만0이면Fizz를 출력하십시오.5의 나머지만0이면Buzz를 출력하십시오.- 그렇지 않으면 숫자를 그대로 출력하십시오.
단계별 따라하기
이 단계별 튜토리얼을 따라가려면, 먼저 Rust를 설치해야 합니다.
🐰 이 게시물의 소스 코드는 GitHub에서 사용할 수 있습니다
Rust를 설치한 후, 터미널 창을 열고 cargo init game을 입력할 수 있습니다.
그런 다음 새로 만들어진 game 디렉토리로 이동합니다.
game├── Cargo.toml└── src └── main.rssrc라는 디렉토리와 main.rs라는 파일을 볼 수 있어야 합니다:
fn main() { println!("Hello, world!");}그 내용을 위의 FizzBuzz 구현으로 교체한 다음 cargo run를 실행합니다.
출력 결과는 다음과 같아야 합니다:
$ cargo run Compiling playground v0.0.1 (/home/bencher) Finished dev [unoptimized + debuginfo] target(s) in 0.44s Running `target/debug/game`
12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzz...9798FizzBuzz🐰 펑! 코딩 인터뷰를 깨고 있습니다!
새로운 Cargo.lock 파일이 생성되어야 합니다:
game├── Cargo.lock├── Cargo.toml└── src └── main.rs더 진행하기 전에, 미세 벤치마킹과 거시 벤치마킹의 차이점을 논의하는 것이 중요합니다.
마이크로-벤치마킹 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에서 지원됩니다. 그렇다면 왜 Gungraun(Iai-Callgrind의 이름이 변경된 후속작)을 선택할까요? Gungraun은 벽시계 시간 대신 명령어 카운트를 사용합니다. 이것은 지속적 벤치마킹, 즉 CI에서의 벤치마킹에 이상적입니다. 특히 공유 러너를 사용하는 경우 지속적 벤치마킹에 Gungraun 사용을 권장합니다. Gungraun은 활발하게 유지보수되며 포괄적인 온라인 문서가 있어 장기 프로젝트에 신뢰할 수 있는 선택입니다. Gungraun은 당신이 정말로 관심 있는 것의 프록시만 측정한다는 것을 이해하는 것이 중요합니다. 1,000 명령어에서 2,000 명령어로 증가하면 애플리케이션의 지연 시간이 두 배가 됩니까? 아마도 그럴 수도 있고 아닐 수도 있습니다. 이러한 이유로, 명령어 카운트 기반 벤치마크와 병렬로 벽시계 시간 기반 벤치마크도 실행하는 것이 유용할 수 있습니다.
Valgrind 설치
Gungraun은 명령어 카운트를 수집하기 위해 Valgrind라는 도구를 사용합니다. Valgrind는 Linux, Solaris, FreeBSD 및 macOS를 지원합니다. 그러나 macOS 지원은 x86_64 프로세서로 제한되며, arm64 (M1, M2 등) 프로세서는 아직 지원되지 않습니다.
Debian에서 실행: sudo apt-get install valgrind
macOS에서 (x86_64/Intel 칩만): brew install valgrind
FizzBuzz 리팩토링
FizzBuzz 애플리케이션을 테스트하기 위해, 프로그램의 main 함수에서 로직을 분리합니다.
다른 벤치마크 하네스와는 달리, Gungraun은 벤치마크 바이너리와
main 함수를 벤치마크할 수 있지만, 이것은 순수하게 매크로 벤치마킹입니다. 우리는 매크로와 마이크로 모두를 원합니다.
이를 위해 몇 가지 변경이 필요합니다.
src 아래에 lib.rs라는 새 파일을 만듭니다:
game├── Cargo.lock├── Cargo.toml└── src └── lib.rs └── main.rslib.rs에 다음 코드를 추가합니다:
pub fn play_game(n: u32, print: bool) { let result = fizz_buzz(n); if print { println!("{result}"); }}
pub fn fizz_buzz(n: u32) -> String { match (n % 3, n % 5) { (0, 0) => "FizzBuzz".to_string(), (0, _) => "Fizz".to_string(), (_, 0) => "Buzz".to_string(), (_, _) => n.to_string(), }}play_game: 부호 없는 정수n을 받아, 해당 숫자로fizz_buzz를 호출하고,print가true이면 결과를 출력합니다.fizz_buzz: 부호 없는 정수n을 받아 실제Fizz,Buzz,FizzBuzz또는 숫자 로직을 수행하고 결과를 문자열로 반환합니다.
그런 다음 업데이트된 main.rs는 다음과 같습니다:
use game::play_game;
fn main() { for i in 1..=100 { play_game(i, true); }}game::play_game:lib.rs로 방금 만든game크레이트에서play_game을 임포트합니다.main: 프로그램의 메인 진입점으로1부터100까지(포함) 숫자를 반복하며 각 숫자에 대해print를true로 설정하여play_game을 호출합니다.
FizzBuzz 벤치마킹
코드를 벤치마크하려면 benches 디렉토리를 만들고
벤치마크를 포함할 파일 play_game.rs를 추가해야 합니다.
간단함을 위해 벤치마크를 구조화하는 권장 방법에서
벗어나고 있다는 점에 유의하세요.
프로젝트에서는 권장 사항을 따르세요:
game├── Cargo.lock├── Cargo.toml└── benches └── play_game.rs└── src └── lib.rs └── main.rsplay_game.rs 안에 다음 코드를 추가합니다:
use gungraun::prelude::*;use std::hint::black_box;use game::play_game;
#[library_benchmark]fn bench_play_game() { for i in 1..=100 { play_game(black_box(i), black_box(false)) }}
library_benchmark_group!( name = bench_play_game_group, benchmarks = [bench_play_game]);
main!(library_benchmark_groups = bench_play_game_group);- 필요한 매크로를 가져오는
gungraun::prelude모듈을 임포트합니다. std::hint::black_box를 사용하여 컴파일러가 벤치마크를 최적화하는 것을 방지합니다.game크레이트에서play_game함수를 임포트합니다.#[library_benchmark]속성을 사용하여bench_play_game이라는 라이브러리 벤치마크 함수를 만듭니다.1부터100까지 루프하며print를false로 설정하여play_game을 호출합니다.bench_play_game벤치마크를 포함하는bench_play_game_group이라는 라이브러리 벤치마크 그룹을 만듭니다.main!매크로를 사용하여 벤치마크 그룹을 실행합니다.
이제 벤치마크를 실행하도록 game 크레이트를 구성해야 합니다.
Cargo.toml 파일의 _하단_에 다음을 추가합니다:
[dev-dependencies]gungraun = "0.18.0"
[[bench]]name = "play_game"harness = false
[profile.bench]debug = truegungraun: 성능 테스트에만 사용하므로gungraun을 개발 의존성으로 추가합니다.bench:play_game을 벤치마크로 등록하고, Gungraun을 벤치마킹 하네스로 사용할 것이므로harness를false로 설정합니다.debug = true: 벤치마크 빌드에서 디버그 정보를 활성화합니다. 이것은 Gungraun이 상세한 출력을 제공하기 위해 필수입니다.
이제 코드를 벤치마크할 준비가 되었습니다. cargo bench를 실행합니다:
$ cargo bench Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)play_game::bench_play_game_group::bench_play_game_100 Instructions: 17902|N/A (*********) L1 Hits: 24984|N/A (*********) LL Hits: 1|N/A (*********) RAM Hits: 20|N/A (*********) Total read+write: 25005|N/A (*********) Estimated Cycles: 25689|N/A (*********)
Gungraun result: Ok. 1 without regressions; 0 regressed; 0 filtered; 1 benchmarks finished in 0.15258s🐰 상추가 순무를 비트로! 첫 번째 벤치마크 메트릭을 얻었습니다!
드디어 피곤한 개발자의 머리를 쉴 수 있습니다… 농담입니다, 사용자들이 새로운 기능을 원합니다!
Rust로 FizzBuzzFibonacci 쓰기
우리의 주요 성과 지표 (KPI)가 하락하여 상품 관리자 (PM)가 새로운 기능을 추가하길 원합니다. 많은 브레인스토밍과 사용자 인터뷰를 거친 후, 예전의 FizzBuzz 만으로는 충분하지 않다는 결론을 내렸습니다. 이 시대의 아이들은 새로운 게임, FizzBuzzFibonacci를 원합니다.
FizzBuzzFibonacci의 규칙은 다음과 같습니다:
1에서100까지의 정수를 출력하는 프로그램을 작성하세요 :
- 3의 배수는
Fizz를 출력합니다.- 5의 배수는
Buzz를 출력합니다.- 3과 5의 배수 모두인 경우에는
FizzBuzz를 출력합니다.- 피보나치 수열의 일부인 숫자는
Fibonacci만 출력합니다.- 그 외의 모든 숫자는 숫자를 출력합니다.
피보나치 수열은 각 숫자가 그 이전 두 숫자의 합계인 수열입니다.
예를 들어, 0과 1에서 시작하여 피보나치 수열의 다음 숫자는 1입니다.
그 다음에는 2, 3, 5, 8 등이 이어집니다.
피보나치 수열의 일부인 숫자를 피보나치 수라고 합니다. 따라서 우리는 피보나치 수를 감지하는 함수를 작성해야 합니다.
피보나치 수열을 작성하는 방법은 많이 있습니다 마찬가지로 피보나치 수를 감지하는 방법도 많습니다. 따라서 제가 가장 선호하는 방법을 선택하겠습니다:
fn is_fibonacci_number(n: u32) -> bool { for i in 0..=n { let (mut previous, mut current) = (0, 1); while current < i { let next = previous + current; previous = current; current = next; } if current == n { return true; } } false}- 부호 없는 정수를 입력으로 받고 불린을 반환하는
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 함수를 업데이트해야 합니다:
pub fn fizz_buzz_fibonacci(n: u32) -> String { if is_fibonacci_number(n) { "Fibonacci".to_string() } else { match (n % 3, n % 5) { (0, 0) => "FizzBuzz".to_string(), (0, _) => "Fizz".to_string(), (_, 0) => "Buzz".to_string(), (_, _) => n.to_string(), } }}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 함수도 업데이트해야 합니다:
pub fn play_game(n: u32, print: bool) { let result = fizz_buzz_fibonacci(n); if print { println!("{result}"); }}우리의 main 함수와 bench_play_game 함수는 그대로 유지될 수 있습니다.
FizzBuzzFibonacci 벤치마킹
이제 벤치마크를 다시 실행할 수 있습니다:
$ cargo bench Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)play_game::bench_play_game_group::bench_play_game_100 Instructions: 331835|17902 (+1753.62%) [+18.5362x] L1 Hits: 338828|24984 (+1256.18%) [+13.5618x] LL Hits: 2|1 (+100.000%) [+2.00000x] RAM Hits: 22|20 (+10.0000%) [+1.10000x] Total read+write: 338852|25005 (+1255.14%) [+13.5514x] Estimated Cycles: 339608|25689 (+1222.00%) [+13.2200x]
Gungraun result: Ok. 1 without regressions; 0 regressed; 0 filtered; 1 benchmarks finished in 0.15254s오, 멋지네요! Gungraun이 FizzBuzz 게임과 FizzBuzzFibonacci 게임의 추정 사이클 차이를 알려줍니다.
여러분의 숫자는 제 것과 약간 다를 것입니다.
그러나 두 게임 간의 차이는 아마 10-15x 범위일 것입니다.
좋아 보입니다! 특히 _Fibonacci_처럼 멋진 기능을 게임에 추가한 것치고는요.
아이들이 좋아할 것입니다!
Rust에서 FizzBuzzFibonacci 확장하기
우리의 게임이 대박입니다! 어린이들이 FizzBuzzFibonacci를 정말 좋아하고 있어요.
그만큼 인기가 많아 익사들로 부터 후속작을 만들라는 소식이 전해졌습니다.
하지만 이건 현대적인 세계, 단번에 구매를 하는 것이 아닌 연간 재발생 수익(ARR)이 필요합니다!
우리 게임의 새로운 비전은 끝이 없는 것입니다, 1과 100 사이의 한계에 묶여 사는 것은 없습니다 (비록 포함된다 해도).
아니요, 우리는 새로운 경계를 넘어가고 있습니다!
Open World FizzBuzzFibonacci의 규칙은 다음과 같습니다:
양의 정수를 입력으로 받아 다음을 출력하는 프로그램을 작성하십시오:
- 세의 배수인 경우
Fizz를 출력합니다- 다섯의 배수인 경우
Buzz를 출력합니다- 세와 다섯의 공배수인 경우
FizzBuzz를 출력합니다- 피보나치 수열에 포함되는 숫자인 경우
Fibonacci만 출력합니다- 그 외의 숫자는 숫자 그대로 출력합니다.
우리의 게임이 어떤 숫자든 작동하도록 하기 위해, 우리는 명령행 인자를 받아들일 필요가 있습니다.
main 함수를 다음과 같이 업데이트합니다:
fn main() { let args: Vec<String> = std::env::args().collect(); let i = args .get(1) .map(|s| s.parse::<u32>()) .unwrap_or(Ok(15)) .unwrap_or(15); play_game(i, true);}- 명령행에서 우리 게임에 전달된 모든 인자 (
args)를 수집합니다. - 우리 게임에 전달된 첫 번째 인자를 부호 없는 정수
i로 파싱합니다. - 파싱이 실패하거나 인자가 전달되지 않으면, 우리 게임을 입력값으로
15를 사용하여 기본으로 실행합니다. - 마지막으로, 새로 파싱된 부호 없는 정수
i로 우리 게임을 실행합니다.
이제 우리는 어떤 숫자로든 우리의 게임을 할 수 있습니다!
명령행 인자를 우리 게임에 전달하려면 --를 따라 cargo run을 사용하세요:
$ cargo run -- 9 Compiling playground v0.0.1 (/home/bencher) Finished dev [unoptimized + debuginfo] target(s) in 0.44s Running `target/debug/game 9`Fizz$ cargo run -- 10 Finished dev [unoptimized + debuginfo] target(s) in 0.03s Running `target/debug/game 10`Buzz$ cargo run -- 13 Finished dev [unoptimized + debuginfo] target(s) in 0.04s Running `target/debug/game 13`Fibonacci그리고 우리가 숫자를 생략하거나 유효하지 않은 숫자를 제공하면:
$ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.03s Running `target/debug/game`FizzBuzz$ cargo run -- bad Finished dev [unoptimized + debuginfo] target(s) in 0.05s Running `target/debug/game bad`FizzBuzz와우, 그것은 상당히 철저한 테스팅 이었습니다! CI가 통과합니다. 우리의 상사들은 매우 만족합니다. 발송해봅시다! 🚀
끝


🐰 … 아마 당신의 경력의 끝일까요?
농담이었어요! 모든 것이 불타고 있어요! 🔥
처음에는 모든 것이 잘 진행된 것처럼 보였어요. 그런데 토요일 새벽 2시 7분에 내 호출기가 울렸습니다:
📟 당신의 게임이 불타고 있어요! 🔥
침대에서 뛰어나와서 무슨 일이 일어나고 있는지 알아내려고 노력했습니다. 로그를 검색하려고 했지만, 모든 것이 계속 충돌해서 검색이 어려웠습니다. 드디어 문제를 찾았습니다. 아이들이었습니다. 아이들이 우리 게임을 너무 좋아해서 백만 번이나 플레이하고 있었습니다! 기백한 두뇌를 깨우고, 두 개의 새로운 벤치마크를 추가했습니다:
여기서 Gungraun의 매개변수화된 벤치마크가 빛을 발합니다! 각 입력에 대해 별도의 벤치마크 함수를 작성하는 대신, #[benches::...] 속성을 사용할 수 있습니다:
#[library_benchmark]#[benches::play(100, 1_000_000)]fn bench_play_game(n: u32) { play_game(black_box(n), black_box(false));}
library_benchmark_group!( name = bench_play_game_group, benchmarks = [bench_play_game_100, bench_play_game]);#[benches::play(100, 1_000_000)]속성을 추가하여 입력100과1_000_000의 벤치마크 변형을 만듭니다.- 벤치마크 함수는 각 값을 받는
n: u32매개변수를 취합니다. bench_play_game함수를library_benchmark_group!에 추가합니다.
좋습니다! 하나의 벤치마크 함수, 여러 테스트 케이스!
실행했을 때, 다음을 얻었습니다:
$ cargo bench Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)play_game::bench_play_game_group::bench_play_game_100 Instructions: 331835|331835 (No change) L1 Hits: 338831|338828 (+0.00089%) [+1.00001x] LL Hits: 1|2 (-50.0000%) [-2.00000x] RAM Hits: 20|22 (-9.09091%) [-1.10000x] Total read+write: 338852|338852 (No change) Estimated Cycles: 339536|339608 (-0.02120%) [-1.00021x]play_game::bench_play_game_group::bench_play_game play_0:(100) Instructions: 7072|N/A (*********) L1 Hits: 7128|N/A (*********) LL Hits: 1|N/A (*********) RAM Hits: 9|N/A (*********) Total read+write: 7138|N/A (*********) Estimated Cycles: 7448|N/A (*********)play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) Instructions: 183930316|N/A (*********) L1 Hits: 183930372|N/A (*********) LL Hits: 1|N/A (*********) RAM Hits: 9|N/A (*********) Total read+write: 183930382|N/A (*********) Estimated Cycles: 183930692|N/A (*********)
Gungraun result: Ok. 3 without regressions; 0 regressed; 0 filtered; 3 benchmarks finished in 1.45441s벤치마크가 1.45초 만에 완료되었습니다. 빨랐네요! 벽시계 벤치마크처럼 벤치마크를
여러 번 실행하는 대신,
각 Gungraun 벤치마크는 한 번만 실행됩니다. 하지만 잠깐, 첫 번째 벤치마크 bench_play_game_100에
변화가 있는 이유는 무엇일까요, 이 벤치마크에서는 아무것도 변경하지 않았는데?
맞습니다, 하지만 벤치마크 파일에서 다른 것을 변경했고 Gungraun과 Valgrind는
민감한 도구이기 때문에 아주 작은 변경도 감지됩니다.
그러나 특히 캐시 메트릭에서의 이러한 작은 변화는 무시할 수 있습니다. 시간이 지남에 따라
메트릭의 중요한 변화에 대한 감각을 키울 수 있습니다. 출력을 자세히 살펴봅시다.
play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) Instructions: 183930316|N/A (*********) L1 Hits: 183930372|N/A (*********) LL Hits: 1|N/A (*********) RAM Hits: 9|N/A (*********) Total read+write: 183930382|N/A (*********) Estimated Cycles: 183930692|N/A (*********)뭐라고! 7,448 추정 사이클 x 1,000은 7,448,000 추정 사이클이어야 하는데 183,930,692 추정 사이클이라니 🤯
피보나치 수열 코드를 기능적으로 올바르게 작성했음에도, 어딘가에 성능 버그가 있는 것 같습니다.
Rust에서 FizzBuzzFibonacci 수정하기
is_fibonacci_number 함수를 다시 한번 살펴보겠습니다:
fn is_fibonacci_number(n: u32) -> bool { for i in 0..=n { let (mut previous, mut current) = (0, 1); while current < i { let next = previous + current; previous = current; current = next; } if current == n { return true; } } false}성능에 대해 생각해보니, 불필요하고 여분의 루프를 가지고 있다는 것을 깨닫게 되었습니다.
for i in 0..=n {} 루프를 완전히 제거하고
주어진 수(n)와 current 값을 그냥 비교할 수 있습니다 🤦
fn is_fibonacci_number(n: u32) -> bool { let (mut previous, mut current) = (0, 1); while current < n { let next = previous + current; previous = current; current = next; } current == n}is_fibonacci_number함수를 업데이트하세요.- 피보나치 수열을
previous와current수를 각각0과1로 시작하여 초기화하세요. - 주어진 수
n보다current숫자가 작은 동안 반복하세요. previous와current숫자를 더하여next숫자를 얻으십시오.previous숫자를current숫자로 업데이트합니다.current숫자를next숫자로 업데이트합니다.current가 주어진 번호n보다 크거나 같으면 루프를 종료합니다.current숫자가 주어진 번호n과 같은지 확인하고 그 결과를 반환합니다.
이제 벤치마크를 다시 실행하고 결과를 봅시다:
$ cargo bench Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)play_game::bench_play_game_group::bench_play_game_100 Instructions: 23679|331835 (-92.8642%) [-14.0139x] L1 Hits: 30675|338831 (-90.9468%) [-11.0458x] LL Hits: 2|1 (+100.000%) [+2.00000x] RAM Hits: 19|20 (-5.00000%) [-1.05263x] Total read+write: 30696|338852 (-90.9412%) [-11.0390x] Estimated Cycles: 31350|339536 (-90.7668%) [-10.8305x]play_game::bench_play_game_group::bench_play_game play_0:(100) Instructions: 218|7072 (-96.9174%) [-32.4404x] L1 Hits: 273|7128 (-96.1700%) [-26.1099x] LL Hits: 1|1 (No change) RAM Hits: 10|9 (+11.1111%) [+1.11111x] Total read+write: 284|7138 (-96.0213%) [-25.1338x] Estimated Cycles: 628|7448 (-91.5682%) [-11.8599x]play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) Instructions: 332|183930316 (-99.9998%) [ -554007x] L1 Hits: 387|183930372 (-99.9998%) [ -475272x] LL Hits: 1|1 (No change) RAM Hits: 10|9 (+11.1111%) [+1.11111x] Total read+write: 398|183930382 (-99.9998%) [ -462137x] Estimated Cycles: 742|183930692 (-99.9996%) [ -247885x]
Gungraun result: Ok. 3 without regressions; 0 regressed; 0 filtered; 3 benchmarks finished in 0.45459s오, 와우! 100 벤치마크가 11% 감소하고 1_000_000 벤치마크는 200,000배 이상 감소했습니다! 183,930,692 추정 사이클에서 742 추정 사이클로!
99.9996% 감소입니다!
🐰 이봐요, 최소한 프로덕션에 도달하기 전에 이 성능 버그를 잡았습니다… 아, 맞다. 신경 쓰지 마세요…
CI에서 성능 저하를 포착하세요
나의 작은 성능 버그로 인해 우리 게임이 받은 부정적인 리뷰들에 대해 경영진들은 만족하지 않았습니다. 그들은 저에게 다시는 그런 일이 발생하지 않도록 하라고 말했고, 어떻게 해야 하는지 물었을 때, 그들은 그냥 다시는 하지 말라고만 말했습니다. 어떻게 그렇게 관리해야 하지요‽
다행스럽게도, 저는 Bencher라는 놀라운 오픈 소스 툴을 발견했습니다. 아주 관대한 무료 티어가 있으므로 저는 개인 프로젝트에 Bencher Cloud를 그냥 사용할 수 있습니다. 그리고 회사에서는 모든 것이 우리의 프라이빗 클라우드에 있어야 하므로, 저는 Bencher Self-Hosted를 사용하기 시작했습니다.
Bencher는 내장 어댑터가 있으므로 CI에 통합하기 쉽습니다. 빠른 시작 가이드를 따른 후, Bencher를 이용해 벤치마크를 실행하고 추적할 수 있습니다.
$ bencher run --project game "cargo bench" Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)play_game::bench_play_game_group::bench_play_game_100 Instructions: 23679|23679 (No change) L1 Hits: 30675|30675 (No change) LL Hits: 2|2 (No change) RAM Hits: 19|19 (No change) Total read+write: 30696|30696 (No change) Estimated Cycles: 31350|31350 (No change)play_game::bench_play_game_group::bench_play_game play_0:(100) Instructions: 218|218 (No change) L1 Hits: 273|273 (No change) LL Hits: 1|1 (No change) RAM Hits: 10|10 (No change) Total read+write: 284|284 (No change) Estimated Cycles: 628|628 (No change)play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) Instructions: 332|332 (No change) L1 Hits: 387|387 (No change) LL Hits: 1|1 (No change) RAM Hits: 10|10 (No change) Total read+write: 398|398 (No change) Estimated Cycles: 742|742 (No change)
Gungraun result: Ok. 3 without regressions; 0 regressed; 0 filtered; 3 benchmarks finished in 0.45370s
Bencher New Report:...View results:- play_game::bench_play_game_group::bench_play_game play_0:(100) (Estimated Cycles): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=f03d9a6c-2b63-45c3-b34a-37149d1a7961&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_0:(100) (Instructions): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=17acf657-735b-4ece-ab32-ba857db5edce&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_0:(100) (L1 Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=009a129f-4476-4202-9e2b-cd7aed7110ac&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_0:(100) (LL Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=932a00d1-e064-4f18-81fb-aa94a5f6d5a0&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_0:(100) (RAM Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=c98672c7-8229-4e90-9773-482618b71dbf&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_0:(100) (Total read+write): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=0bd6ec91-2b29-47ea-801e-dc09338f3119&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (Estimated Cycles): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=f03d9a6c-2b63-45c3-b34a-37149d1a7961&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (Instructions): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=17acf657-735b-4ece-ab32-ba857db5edce&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (L1 Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=009a129f-4476-4202-9e2b-cd7aed7110ac&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (LL Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=932a00d1-e064-4f18-81fb-aa94a5f6d5a0&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (RAM Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=c98672c7-8229-4e90-9773-482618b71dbf&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (Total read+write): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=0bd6ec91-2b29-47ea-801e-dc09338f3119&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (Estimated Cycles): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=f03d9a6c-2b63-45c3-b34a-37149d1a7961&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (Instructions): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=17acf657-735b-4ece-ab32-ba857db5edce&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (L1 Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=009a129f-4476-4202-9e2b-cd7aed7110ac&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (LL Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=932a00d1-e064-4f18-81fb-aa94a5f6d5a0&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (RAM Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=c98672c7-8229-4e90-9773-482618b71dbf&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (Total read+write): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=0bd6ec91-2b29-47ea-801e-dc09338f3119&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409이 멋진 시간 여행 장치를 통해, 친절한 토끼가 내게 준 것을 활용하여, Bencher를 계속 사용했더라면 무슨 일이 일어났을지 되돌려보았습니다. 처음으로 버그가 있는 FizzBuzzFibonacci 구현을 푸시한 곳을 볼 수 있습니다. 나는 즉시 내 풀 요청에 대해 CI에서 실패를 받았습니다. 그날 저녁, 저는 그 불필요하고, 추가적인 반복문을 제거함으로써 성능 버그를 수정했습니다. 화재는 없습니다. 그저 행복한 사용자들만 있습니다.
Bencher: 지속적인 벤치마킹
Bencher는 지속적인 벤치마킹 도구 모음입니다. 성능 회귀가 사용자에게 영향을 미친 경험이 있나요? Bencher가 그런 일이 일어나는 것을 막을 수 있었습니다. Bencher를 이용하면 성능 회귀를 병합되기 전에 탐지하고 예방할 수 있습니다.
- 실행: 완전히 동일한 베어 메탈 러너와 즐겨 사용하는 벤치마킹 도구를 사용하여 로컬 또는 CI에서 벤치마크를 실행합니다.
bencherCLI는 베어 메탈에서 벤치마크 실행을 조율하고 결과를 저장합니다. - 추적: 벤치마크 결과를 시간이 지남에 따라 추적합니다. 소스 브랜치, 테스트 베드, 측정 기반의 Bencher 웹 콘솔을 사용하여 결과를 모니터링, 쿼리, 그래프로 만듭니다.
- 캐치: 완전히 동일한 베어 메탈 하드웨어를 사용하여 로컬 또는 CI에서 성능 회귀를 잡아냅니다. Bencher는 최첨단, 사용자 정의 가능한 분석을 사용하여 병합되기 전에 성능 회귀를 탐지합니다.
단위 테스트가 기능 회귀를 방지하기 위해 실행되는 것처럼, 벤치마크는 Bencher와 함께 실행되어 성능 회귀를 방지해야 합니다. 성능 버그도 버그입니다!
성능 회귀를 잡아내기 시작하세요 - Bencher Cloud를 무료로 시도해보세요.