Rust 코드를 Iai로 벤치마킹하는 방법


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

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

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가 지원합니다. 그렇다면 왜 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라는 새 파일을 생성하세요:

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 game::play_game;
fn bench_play_game() {
iai::black_box(for i in 1..=100 {
play_game(i, false)
});
}
iai::main!(bench_play_game);
  • game 크레이트에서 play_game 함수를 import합니다.
  • bench_play_game이라는 함수를 생성합니다.
  • 컴파일러가 우리의 코드를 최적화하지 못하도록 “black box” 내에서 우리의 macro-benchmark를 실행합니다.
  • 1부터 100까지 순차적으로 반복합니다.
  • 각 숫자에 대해 play_game을 호출하고, printfalse로 설정합니다.

이제 벤치마크를 실행하기 위해 game 크레이트를 설정해야 합니다.

Cargo.toml 파일의 _바닥_에 다음을 추가합니다:

[dev-dependencies]
iai = "0.1"
[[bench]]
name = "play_game"
harness = false
  • iai: iai를 개발 종속성으로 추가합니다. 왜냐하면 우리는 이를 성능 테스팅을 위해 사용하기 때문입니다.
  • bench: play_game을 벤치마크로 등록하고 harnessfalse로 설정합니다. 왜냐하면 벤치마킹 하네스로 Iai를 사용할 것이기 때문입니다.

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

$ cargo bench
Compiling iai v0.1.1
Compiling game v0.1.0 (/home/bencher)
Finished bench [optimized] target(s) in 2.55s
Running unittests src/lib.rs (target/release/deps/game-9b1b504669ca4b29)
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-8d61ca5a97299729)
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-6896309faf45cd96)
bench_play_game
Instructions: 34370
L1 Accesses: 50373
L2 Accesses: 9
RAM Accesses: 35
Estimated Cycles: 51643

🐰 Lettuce turnip the beet! 우리는 첫 벤치마크 메트릭을 얻었습니다!

드디어, 우리는 지친 개발자 머리를 쉴 수 있습니다… 농담입니다, 우리 사용자들은 새로운 기능을 원합니다!

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 game v0.1.0 (/home/bencher)
Finished bench [optimized] target(s) in 2.20s
Running unittests src/lib.rs (target/release/deps/game-9b1b504669ca4b29)
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-8d61ca5a97299729)
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-6896309faf45cd96)
bench_play_game
Instructions: 304598 (+786.2322%)
L1 Accesses: 320024 (+535.3086%)
L2 Accesses: 8 (-11.11111%)
RAM Accesses: 42 (+20.00000%)
Estimated Cycles: 321534 (+522.6091%)

오, 깔끔! Iai는 우리의 FizzBuzz와 FizzBuzzFibonacci 게임의 추정된 사이클 사이의 차이는 +522.6091%라고 알려줍니다. 당신의 숫자는 제 숫자와 약간 다를 것입니다. 그러나 두 게임 사이의 차이는 아마 5x 범위에 있을 겁니다. 그게 좋은 것 같아요! 특히 우리 게임에 _Fibonacci_처럼 멋진 기능을 추가했을 때 말이죠. 아이들이 이것을 좋아할 거예요!

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() {
iai::black_box(play_game(100, false));
}
fn bench_play_game_1_000_000() {
iai::black_box(play_game(1_000_000, false));
}
  • 숫자 백 (100)으로 게임을 플레이하는 마이크로 벤치마크 bench_play_game_100
  • 숫자 백만 (1_000_000)으로 게임을 플레이하는 마이크로 벤치마크 bench_play_game_1_000_000

그것을 실행했을 때, 저는 이렇게 받았습니다:

$ cargo bench
Compiling game v0.1.0 (/home/bencher)
Finished bench [optimized] target(s) in 1.92s
Running unittests src/lib.rs (target/release/deps/game-9b1b504669ca4b29)
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-8d61ca5a97299729)
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-6896309faf45cd96)
bench_play_game
Instructions: 304598 (No change)
L1 Accesses: 320025 (+0.000312%)
L2 Accesses: 7 (-12.50000%)
RAM Accesses: 42 (No change)
Estimated Cycles: 321530 (-0.001244%)
bench_play_game_100
Instructions: 6194
L1 Accesses: 6290
L2 Accesses: 2
RAM Accesses: 11
Estimated Cycles: 6685

기다리고… 기다리고…

bench_play_game_1_000_000
Instructions: 155108715
L1 Accesses: 155108811
L2 Accesses: 2
RAM Accesses: 11
Estimated Cycles: 155109206

무엇! 6,685 추정 사이클 x 1,0006,685,000 추정 사이클이어야 하지 155,109,206 추정 사이클이 아니야 🤯 비록 나의 피보나치 수열 코드가 기능적으로 정확하다고 해도, 어딘가에 성능 버그가 있어야 합니다.

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 game v0.1.0 (/home/bencher)
Finished bench [optimized] target(s) in 4.22s
Running unittests src/lib.rs (target/release/deps/game-9b1b504669ca4b29)
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-8d61ca5a97299729)
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-6896309faf45cd96)
bench_play_game
Instructions: 38313 (-87.42178%)
L1 Accesses: 53739 (-83.20787%)
L2 Accesses: 7 (No change)
RAM Accesses: 43 (+2.380952%)
Estimated Cycles: 55279 (-82.80751%)
bench_play_game_100
Instructions: 295 (-95.23733%)
L1 Accesses: 389 (-93.81558%)
L2 Accesses: 2 (No change)
RAM Accesses: 13 (+18.18182%)
Estimated Cycles: 854 (-87.22513%)
bench_play_game_1_000_000
Instructions: 391 (-99.99975%)
L1 Accesses: 485 (-99.99969%)
L2 Accesses: 2 (No change)
RAM Accesses: 13 (+18.18182%)
Estimated Cycles: 950 (-99.99939%)

오, 와우! 우리의 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 run --project game "cargo bench"
Finished bench [optimized] target(s) in 0.18s
Running unittests src/lib.rs (target/release/deps/game-9b1b504669ca4b29)
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-8d61ca5a97299729)
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-6896309faf45cd96)
bench_play_game
Instructions: 38331 (+0.046981%)
L1 Accesses: 53765 (+0.048382%)
L2 Accesses: 6 (-14.28571%)
RAM Accesses: 45 (+4.651163%)
Estimated Cycles: 55370 (+0.164619%)
bench_play_game_100
Instructions: 313 (+6.101695%)
L1 Accesses: 416 (+6.940874%)
L2 Accesses: 2 (No change)
RAM Accesses: 13 (No change)
Estimated Cycles: 881 (+3.161593%)
bench_play_game_1_000_000
Instructions: 409 (+4.603581%)
L1 Accesses: 512 (+5.567010%)
L2 Accesses: 2 (No change)
RAM Accesses: 13 (No change)
Estimated Cycles: 977 (+2.842105%)
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
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에서 문제를 열어주세요.