Rust 의 장점 — 고르면 무엇을 얻나
Rust 를 고르면 메모리 안전을 컴파일할 때 얻고, 가비지 컬렉터 없이 빠르게 돌고, 런타임 없는 실행 파일 하나로 배포한다. 이 편에서는 그 가운데 잴 수 있는 것을 직접 잰다. release 빌드의 속도, 반복자와 손으로 쓴 반복문의 속도, 실행 파일의 크기다.
이 시리즈는 Rust 입문·중급 47편을 마친 독자를 생각하고 쓴다. 문법은 그쪽에서 다뤘다.
01 1. 메모리 안전을 컴파일할 때 얻는다
C·C++ 의 보안 사고 상당수는 해제한 메모리를 다시 읽거나, 배열 밖을 쓰는 메모리 오류다. Rust 는 소유권·빌림 규칙(입문 15~19편)으로 이런 코드를 컴파일 단계에서 막는다. 가비지 컬렉터 없이 이것을 해낸다는 것이 다른 언어와 가장 크게 다른 점이다.
실제 효과를 숫자로 낸 곳이 안드로이드다. 구글 보안 블로그(2025-11-13)는 안드로이드에서 메모리 안전 취약점의 비중이 처음으로 전체의 20% 아래로 내려갔고, Rust 코드의 메모리 안전 취약점 밀도가 C·C++ 코드보다 약 1000배 낮다고 밝혔다. 같은 글에서 Rust 변경은 C++ 보다 코드 리뷰에 약 25% 시간을 덜 쓰고, 중·대형 변경의 되돌림 비율이 약 4배 낮다고 했다. 안전이 속도를 늦추기만 하는 것은 아니라는 뜻이다.
02 2. 빠르다 — 그리고 release 빌드를 잊지 않는다
5천만 이하의 소수를 세는 프로그램(에라토스테네스의 체)을 debug 와 release 로 각각 돌렸다.
use std::time::Instant;
fn count_primes(limit: usize) -> usize {
let mut is_prime = vec![true; limit + 1];
is_prime[0] = false;
is_prime[1] = false;
let mut i = 2;
while i * i <= limit {
if is_prime[i] {
let mut j = i * i;
while j <= limit {
is_prime[j] = false;
j += i;
}
}
i += 1;
}
is_prime.iter().filter(|&&p| p).count()
}
fn main() {
let start = Instant::now();
let n = count_primes(50_000_000);
println!("{n} primes, {} ms", start.elapsed().as_millis());
}cargo run Compiling primes v0.1.0 (~/projects/primes)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s
Running `target/debug/primes`
3001134 primes, 1179 mscargo run --release Compiling primes v0.1.0 (~/projects/primes)
Finished `release` profile [optimized] target(s) in 0.23s
Running `target/release/primes`
3001134 primes, 151 ms같은 코드가 debug 1200ms, release 167ms 로 7배쯤 차이가 난다. cargo run 의 기본 debug 빌드는 컴파일을 빨리 하려고 최적화를 거의 하지 않는다. 속도를 잴 때, 배포할 때는 반드시 --release 다. 숫자는 기계와 실행마다 조금씩 다르다.
Rust 는 기계어로 컴파일되고 가비지 컬렉터가 없다. 그래서 GC 가 갑자기 멈추는 일(지연 튐)이 없다. 디스코드는 2020년 글에서 Go 로 만든 읽음 상태 서비스가 "적어도 2분마다 GC 를 강제로 돈다" 는 것 때문에 지연이 튀었고, Rust 로 옮긴 뒤 다듬어서 "모든 성능 지표에서 Go 를 이겼다" 고 썼다.
03 3. 추상화에 값을 치르지 않는다
37편의 반복자는 읽기 쉽다. 그런데 느리지는 않을까. 같은 계산을 손으로 쓴 while 과 반복자로 써서 release 로 쟀다.
use std::hint::black_box;
use std::time::Instant;
fn with_loop(n: u64) -> u64 {
let mut total = 0;
let mut i = 0;
while i < n {
if i % 3 == 0 {
total += i * i;
}
i += 1;
}
total
}
fn with_iter(n: u64) -> u64 {
(0..n).filter(|i| i % 3 == 0).map(|i| i * i).sum()
}
fn main() {
let n = black_box(300_000_000);
let start = Instant::now();
let a = with_loop(n);
println!("loop: {a} in {} ms", start.elapsed().as_millis());
let start = Instant::now();
let b = with_iter(n);
println!("iter: {b} in {} ms", start.elapsed().as_millis());
} Compiling sums v0.1.0 (~/projects/sums)
Finished `release` profile [optimized] target(s) in 0.19s
Running `target/release/sums`
loop: 5966292615770689920 in 235 ms
iter: 5966292615770689920 in 207 ms결과가 같고 시간도 비슷하다(반복자 쪽이 오히려 조금 빨랐다). 컴파일러가 반복자를 손으로 쓴 반복문과 같은 기계어로 풀어 주기 때문이다. Rust 에서는 이것을 "공짜 추상화(zero-cost abstraction)" 라고 부른다. std::hint::black_box 는 컴파일러가 입력값을 미리 알고 계산을 통째로 없애 버리지 못하게 막는 장치다. 측정용으로만 쓴다.
04 4. 런타임 없는 실행 파일 하나
Compiling hello v0.1.0 (~/projects/hello)
Finished `release` profile [optimized] target(s) in 0.48s
431200cargo build --release 로 만든 hello 실행 파일은 431,200바이트(약 420KB)다. 이 파일 하나만 복사하면 다른 기계(같은 운영체제·CPU)에서 바로 돈다. JVM·Node.js·Python 인터프리터 같은 런타임을 따로 깔 필요가 없다. 컨테이너 이미지를 작게 만들거나, 명령줄 도구를 나눠 줄 때 이 점이 크다.
05 실습 — 도구가 한 벌로 온다
마지막 장점은 도구다. cargo 하나로 빌드·실행·테스트(입문 42편)·의존성(41편)·문서·포맷·린트(이 시리즈 9편)를 모두 한다. 언어마다 빌드 도구와 패키지 관리자를 따로 고르는 수고가 없다.
위의 primes 를 직접 만들어 debug·release 로 돌려 보자. 숫자는 기계마다 다르지만 몇 배 차이는 비슷하게 날 것이다. 다음 편은 반대로, Rust 를 고르면 치르는 값이다.