Criterion을 이용한 Rust 코드 벤치마킹 방법


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

벤치마킹은 코드의 성능을 테스트하며 얼마나 빠르게 (지연 시간) 또는 얼마나 많은 작업 (처리량)을 할 수 있는지 파악하는 행동입니다. 이는 종종 소프트웨어 개발에서 무시되는 단계이지만, 빠르고 최적화된 코드를 생성하고 유지하는 데 필수적입니다. 벤치마킹은 개발자들이 다양한 작업 부하와 조건 하에서 코드가 얼마나 잘 수행되는지 이해하는 데 필요한 메트릭을 제공합니다. 기능 회귀를 방지하기 위해 단위 테스트와 통합 테스트를 작성하는 것처럼, 성능 회귀를 방지하기 위해 벤치마크를 작성해야 합니다. 성능 버그는 버그입니다!

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까지 포함하여 반복하세요.
  • 각 숫자에 대해 35 모두에 대한 나머지(나눗셈 후 나머지)를 계산하십시오.
  • 두 나머지에 대해 패턴 매치를 합니다. 나머지가 0이면 해당 숫자는 주어진 인수의 배수입니다.
  • 35 모두의 나머지가 0이면 FizzBuzz를 출력하십시오.
  • 3의 나머지만 0이면 Fizz를 출력하십시오.
  • 5의 나머지만 0이면 Buzz를 출력하십시오.
  • 그렇지 않으면 숫자를 그대로 출력하십시오.

단계별 따라하기

이 단계별 튜토리얼을 따라가려면, 먼저 Rust를 설치해야 합니다.

🐰 이 게시물의 소스 코드는 GitHub에서 사용할 수 있습니다

Rust를 설치한 후, 터미널 창을 열고 cargo init game을 입력할 수 있습니다.

그런 다음 새로 만들어진 game 디렉토리로 이동합니다.

game
├── Cargo.toml
└── src
└── main.rs

src라는 디렉토리와 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`
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
...
97
98
Fizz
Buzz

🐰 펑! 코딩 인터뷰를 깨고 있습니다!

새로운 Cargo.lock 파일이 생성되어야 합니다:

game
├── Cargo.lock
├── Cargo.toml
└── src
└── main.rs

더 진행하기 전에, 미세 벤치마킹과 거시 벤치마킹의 차이점을 논의하는 것이 중요합니다.

마이크로-벤치마킹 vs 매크로-벤치마킹

소프트웨어 벤치마크의 두 가지 주요 카테고리는 마이크로-벤치마크와 매크로-벤치마크입니다. 마이크로-벤치마크는 단위 테스트와 유사한 수준에서 작동합니다. 예를 들어, 단일 숫자에 대해 Fizz, Buzz, 또는 FizzBuzz를 결정하는 함수에 대한 벤치마크는 마이크로-벤치마크가 될 것입니다. 매크로-벤치마크는 통합 테스트와 유사한 수준에서 작동합니다. 예를 들어, 1에서 100까지의 FizzBuzz 게임을 전체적으로 실행하는 함수의 벤치마크는 매크로-벤치마크가 될 것입니다.

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

Rust에서의 벤치마킹

Rust에서 벤치마킹에 대해 일반적으로 선호하는 세 가지 옵션은 libtest bench, Criterion, 그리고 Iai입니다.

libtest는 Rust의 내장 유닛 테스트 및 벤치마킹 프레임워크입니다. Rust 표준 라이브러리의 일부임에도 불구하고, libtest bench는 여전히 불안정하다고 판단되므로, nightly 컴파일러 릴리스에서만 사용 가능합니다. 안정적인 Rust 컴파일러에서 작업하려면, 별도의 벤치마킹 하네스를 사용해야 합니다. 그러나 둘 다 적극적으로 개발되고 있지는 않습니다.

Rust 생태계 내에서 가장 활발히 유지 관리되는 벤치마킹 하네스는 Criterion입니다. 안정적인 릴리스와 nightly Rust 컴파일러 릴리스 모두에서 작동하며, Rust 커뮤니티 내에서 사실상의 표준이 되었습니다. 또한 Criterion은 libtest bench와 비교하여 훨씬 더 풍부한 기능을 제공합니다.

Criterion의 실험적인 대안으로서 동일한 생성자가 만든 Iai가 있습니다. 그러나, 벽 시계 시간 대신 명령어 수를 사용합니다: CPU 명령어, L1 접근, L2 접근 및 RAM 접근. 이를 통해 단일 샷 벤치마킹을 할 수 있으며, 이러한 메트릭은 실행 사이에 거의 동일하게 유지되어야 합니다.

세 가지 모두 Bencher에 의해 지원됩니다.그렇다면 왜 Criterion을 선택해야 할까요? Criterion은 Rust 커뮤니티의 벤치마킹용 표준 도구입니다. 코드의 지연 시간을 벤치마킹하는 데 Criterion을 사용하는 것을 추천합니다. 즉, Criterion은 벽시계 시간을 측정하는 데 탁월합니다.

FizzBuzz 재구성

FizzBuzz 애플리케이션을 테스트하기 위해서는 우리의 로직을 프로그램의 main 함수에서 분리해야 합니다. 벤치마크 하니스는 main 함수를 벤치마크할 수 없습니다. 이를 위해서는 몇 가지 변경이 필요합니다.

src 내에 lib.rs라는 새 파일을 생성하세요:

game
├── Cargo.lock
├── Cargo.toml
└── src
└── lib.rs
└── main.rs

다음의 코드를 lib.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를 해당 숫자와 함께 호출하고, printtrue일 경우 결과를 출력합니다.
  • 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까지 반복하면서 각 숫자에 대해 printtrue로 설정한 상태로 play_game를 호출합니다.

FizzBuzz 벤치마킹

코드를 벤치마킹하기 위해 benches 디렉토리를 생성하고 벤치마크를 포함하는 파일 play_game.rs를 추가해야 합니다:

game
├── Cargo.lock
├── Cargo.toml
└── benches
└── play_game.rs
└── src
└── lib.rs
└── main.rs

play_game.rs에 다음 코드를 추가합니다:

use criterion::{criterion_group, criterion_main, Criterion};
use game::play_game;
fn bench_play_game(c: &mut Criterion) {
c.bench_function("bench_play_game", |b| {
b.iter(|| {
std::hint::black_box(for i in 1..=100 {
play_game(i, false)
});
});
});
}
criterion_group!(
benches,
bench_play_game,
);
criterion_main!(benches);

자, 이제 game 크레이트를 구성하여 벤치마킹을 실행할 준비를 해봅시다.

Cargo.toml 파일의 _하단_에 다음을 추가해주세요:

[dev-dependencies]
criterion = "0.5"
[[bench]]
name = "play_game"
harness = false
  • criterion: criterion를 개발 의존성으로 추가하였습니다. 이는 우리가 성능 테스팅에만 이를 사용하기 때문입니다.
  • bench: play_game을 벤치마크로 등록하고 harnessfalse로 설정하였습니다. 이는 우리가 Criterion을 벤치마킹 하네스로 사용할 것이기 때문입니다.

이제 코드를 벤치마킹할 준비가 되었습니다. cargo bench를 실행해보세요:

$ cargo bench
Compiling playground v0.0.1 (/home/bencher)
Finished bench [optimized] target(s) in 4.79s
Running unittests src/main.rs (target/release/deps/game-68f58c96f4025bd4)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/release/deps/game-043972c4132076a9)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running benches/play_game.rs (target/release/deps/play_game-e0857103eb02eb56)
bench_play_game time: [3.0020 µs 3.0781 µs 3.1730 µs]
Found 12 outliers among 100 measurements (12.00%)
2 (2.00%) high mild
10 (10.00%) high severe

🐰 우리는 이제 첫 번째 벤치마크 메트릭을 가지게 되었습니다!

드디어 우리는 지친 개발자의 머리를 쉬게 해줄 수 있습니다… 아마도, 우리의 사용자들은 새로운 기능을 원합니다!

Rust로 FizzBuzzFibonacci 쓰기

우리의 주요 성과 지표 (KPI)가 하락하여 상품 관리자 (PM)가 새로운 기능을 추가하길 원합니다. 많은 브레인스토밍과 사용자 인터뷰를 거친 후, 예전의 FizzBuzz 만으로는 충분하지 않다는 결론을 내렸습니다. 이 시대의 아이들은 새로운 게임, FizzBuzzFibonacci를 원합니다.

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

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

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

피보나치 수열은 각 숫자가 그 이전 두 숫자의 합계인 수열입니다. 예를 들어, 01에서 시작하여 피보나치 수열의 다음 숫자는 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부터 모든 숫자에 대해 반복합니다.
  • previouscurrent 숫자를 각각 01로 시작하는 피보나치 수열을 초기화합니다.
  • current 숫자가 현재 반복 i보다 작은 동안 반복합니다.
  • previouscurrent 숫자를 더하여 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_buzzfizz_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
Compiling playground v0.0.1 (/home/bencher)
Finished bench [optimized] target(s) in 4.79s
Running unittests src/main.rs (target/release/deps/game-68f58c96f4025bd4)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/release/deps/game-043972c4132076a9)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running benches/play_game.rs (target/release/deps/play_game-e0857103eb02eb56)
bench_play_game time: [20.067 µs 20.107 µs 20.149 µs]
change: [+557.22% +568.69% +577.93%] (p = 0.00 < 0.05)
Performance has regressed.
Found 6 outliers among 100 measurements (6.00%)
4 (4.00%) high mild
2 (2.00%) high severe

오, 대단하네! Criterion은 FizzBuzz와 FizzBuzzFibonacci 게임의 성능 차이가 +568.69%라고 말해줍니다. 당신의 숫자들은 제 것과 약간 다를 수도 있습니다. 그러나 두 게임간의 차이는 5x 범위 안에 있을 가능성이 높습니다. 그것은 나에게 좋아 보입니다! 특히 우리의 게임에 이렇게 멋진 기능인 _피보나치_를 추가하는 것에 대한 대가로 말이죠. 아이들은 그것을 사랑할 것입니다!

Rust에서 FizzBuzzFibonacci 확장하기

우리의 게임이 대박입니다! 어린이들이 FizzBuzzFibonacci를 정말 좋아하고 있어요. 그만큼 인기가 많아 익사들로 부터 후속작을 만들라는 소식이 전해졌습니다. 하지만 이건 현대적인 세계, 단번에 구매를 하는 것이 아닌 연간 재발생 수익(ARR)이 필요합니다! 우리 게임의 새로운 비전은 끝이 없는 것입니다, 1100 사이의 한계에 묶여 사는 것은 없습니다 (비록 포함된다 해도). 아니요, 우리는 새로운 경계를 넘어가고 있습니다!

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가 통과합니다. 우리의 상사들은 매우 만족합니다. 발송해봅시다! 🚀


SpongeBob SquarePants Three Weeks Later
This is Fine meme

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


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

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

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

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

fn bench_play_game_100(c: &mut Criterion) {
c.bench_function("bench_play_game_100", |b| {
b.iter(|| std::hint::black_box(play_game(100, false)));
});
}
fn bench_play_game_1_000_000(c: &mut Criterion) {
c.bench_function("bench_play_game_1_000_000", |b| {
b.iter(|| std::hint::black_box(play_game(1_000_000, false)));
});
}
  • 숫자 100(100)으로 게임을 하는 마이크로벤치마크 bench_play_game_100
  • 숫자 백만(1,000,000)으로 게임을 하는 마이크로벤치마크 bench_play_game_1_000_000

저는 이 현상을 실행 했을때 다음과 같은 결과를 얻었습니다:

$ cargo bench
Compiling playground v0.0.1 (/home/bencher)
Finished bench [optimized] target(s) in 4.79s
Running unittests src/main.rs (target/release/deps/game-68f58c96f4025bd4)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/release/deps/game-043972c4132076a9)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running benches/play_game.rs (target/release/deps/play_game-e0857103eb02eb56)
bench_play_game time: [20.024 µs 20.058 µs 20.096 µs]
change: [-0.0801% +0.1431% +0.3734%] (p = 0.21 > 0.05)
No change in performance detected.
Found 17 outliers among 100 measurements (17.00%)
9 (9.00%) high mild
8 (8.00%) high severe
bench_play_game_100 time: [403.00 ns 403.57 ns 404.27 ns]
Found 13 outliers among 100 measurements (13.00%)
6 (6.00%) high mild
7 (7.00%) high severe

잠깐만 기다려보세요…

bench_play_game_1_000_000
time: [9.5865 ms 9.5968 ms 9.6087 ms]
Found 16 outliers among 100 measurements (16.00%)
8 (8.00%) high mild
8 (8.00%) high severe

무엇! 403.57 ns x 1,000403,570 ns이어야 하고 9,596,800 ns (9.5968 ms x 1_000_000 ns/1 ms)는 아니어야 합니다. 🤯 처럼 보이는데 제 피보나치 수열 코드는 기능적으론 맞게 작성되었지만, 어딘가에 성능 버그가 있어 보입니다.

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 함수를 업데이트하세요.
  • 피보나치 수열을 previouscurrent 수를 각각 01 로 시작하여 초기화하세요.
  • 주어진 수 n 보다 current 숫자가 작은 동안 반복하세요.
  • previouscurrent 숫자를 더하여 next 숫자를 얻으십시오.
  • previous 숫자를 current 숫자로 업데이트합니다.
  • current 숫자를 next 숫자로 업데이트합니다.
  • current가 주어진 번호 n보다 크거나 같으면 루프를 종료합니다.
  • current 숫자가 주어진 번호 n과 같은지 확인하고 그 결과를 반환합니다.

자, 이제 벤치마크를 다시 실행하고 어떻게 되는지 알아봅시다:

$ cargo bench
Compiling playground v0.0.1 (/home/bencher)
Finished bench [optimized] target(s) in 4.79s
Running unittests src/main.rs (target/release/deps/game-68f58c96f4025bd4)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/release/deps/game-043972c4132076a9)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running benches/play_game.rs (target/release/deps/play_game-e0857103eb02eb56)
bench_play_game time: [3.1201 µs 3.1772 µs 3.2536 µs]
change: [-84.469% -84.286% -84.016%] (p = 0.00 < 0.05)
Performance has improved.
Found 5 outliers among 100 measurements (5.00%)
1 (1.00%) high mild
4 (4.00%) high severe
bench_play_game_100 time: [24.460 ns 24.555 ns 24.650 ns]
change: [-93.976% -93.950% -93.927%] (p = 0.00 < 0.05)
Performance has improved.
bench_play_game_1_000_000
time: [30.260 ns 30.403 ns 30.564 ns]
change: [-100.000% -100.000% -100.000%] (p = 0.00 < 0.05)
Performance has improved.
Found 4 outliers among 100 measurements (4.00%)
1 (1.00%) high mild
3 (3.00%) high severe

오, 와우! 우리의 bench_play_game 벤치마크는 원래의 FizzBuzz 벤치마크 결과 값 쯤으로 돌아와 있습니다. 그 점수가 어떤 값이었는지 기억할 수 있으면 좋겠는데, 벌써 3주가 지났습니다. 내 터미널 기록은 그 정도 까지 돌아가지 않아요. 그리고 Criterion은 가장 최근의 결과값만 비교합니다. 하지만 그 차이는 비슷할 것 같아요!

bench_play_game_100 벤치마크는 거의 10배 가량 떨어졌습니다, -93.950%. 그리고 bench_play_game_1_000_000 벤치마크는 10,000배 이상 떨어졌습니다! 9,596,800 ns 에서 30.403 ns로! 우리는 심지어 Criterion의 변화율 게이지를 최대치로 올렸습니다. 이는 -100.000%까지만 계산되기 때문입니다!

🐰 헤이, 적어도 우리는 이 성능 버그를 제품이 출시되기 전에 잡아낸 것 같네요…아, 그래도 없었나봐요…

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

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

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

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

$ bencher run --project game "cargo bench"
Finished bench [optimized] target(s) in 0.07s
Running unittests src/lib.rs (target/release/deps/game-13f4bad779fbfde4)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/release/deps/game-043972c4132076a9)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running benches/play_game.rs (target/release/deps/play_game-e0857103eb02eb56)
Gnuplot not found, using plotters backend
bench_play_game time: [3.0713 µs 3.0902 µs 3.1132 µs]
Found 16 outliers among 100 measurements (16.00%)
3 (3.00%) high mild
13 (13.00%) high severe
bench_play_game_100 time: [23.938 ns 23.970 ns 24.009 ns]
Found 15 outliers among 100 measurements (15.00%)
5 (5.00%) high mild
10 (10.00%) high severe
bench_play_game_1_000_000
time: [30.004 ns 30.127 ns 30.279 ns]
Found 5 outliers among 100 measurements (5.00%)
1 (1.00%) high mild
4 (4.00%) high severe
Bencher New Report:
...
View results:
- bench_play_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
- bench_play_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
- bench_play_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에서 문제를 열어주세요.