为何Rust版FizzBuzz代码比Node.js慢?如何优化?
我写了一个生成1到n的FizzBuzz序列的Rust函数,同时写了等效的JavaScript函数做基准测试。结果发现Node.js运行速度明显快于Rust,而且我已经用cargo run -r在release模式下编译运行Rust代码了。
测试数据
- 当n=100、重复执行1e6次时:
- Node.js:707ms
- Rust:1047ms
- 当n=1000、重复执行1e6次时:
- Node.js:6.7s
- Rust:11s
想请教两个问题:
- 怎么改写Rust代码提升性能?
- Node(V8引擎)是怎么优化代码实现这么快的速度的?
相关代码
JavaScript代码
const fizzBuzz = n => { let res = '1' for (let i = 2; i < n; i++) { res += ' ' if (i % 15 === 0) { res += 'fizzbuzz' } else if (i % 3 === 0) { res += 'fizz' } else if (i % 5 === 0) { res += 'buzz' } else { res += i } } return res } const perf = (f, n) => { console.time() for (let i = 0; i < n; i++) { f() } console.timeEnd() } perf(() => fizzBuzz(100), 1e6) perf(() => fizzBuzz(1000), 1e6) // default: 707.632ms // default: 6.683s
原始Rust代码
use std::time::Instant; fn calc_capacity(n: f32) -> f32 { n // spaces + (n / 30.0 * 12.0) * 4.0 // fizz buzz + (n / 30.0 * 2.0) * 8.0 // fizzbuzz + (n / 30.0 * 16.0) * f32::log10(n).ceil() // numbers } pub fn fizz_buzz(n: u32) -> String { let mut res = String::with_capacity(calc_capacity(n as f32) as usize); res.push('1'); for i in 2..n { res.push(' '); if i % 15 == 0 { res.push_str("fizzbuzz"); } else if i % 3 == 0 { res.push_str("fizz"); } else if i % 5 == 0 { res.push_str("buzz"); } else { res.push_str(&i.to_string()); } } res } pub fn perf(f: &dyn Fn(), n: u32) { let start_time = Instant::now(); for _ in 0..n { f(); } let end_time = Instant::now(); println!("{}ms", end_time.duration_since(start_time).as_millis()); } fn main() { perf( &|| { fizz_buzz(100); }, 1e6 as u32, ); perf( &|| { fizz_buzz(1000); }, 1e6 as u32, ); // 1047ms // 10927ms }
优化尝试的Rust代码
const FIZZ: &[u8; 4] = &[102, 105, 122, 122]; const BUZZ: &[u8; 4] = &[98, 117, 122, 122]; pub fn fizz_buzz_2(n: u32) -> String { let mut res = String::with_capacity(calc_capacity(n as f32) as usize); let vec = unsafe { res.as_mut_vec() }; vec.push(b'1'); for i in 2..n { vec.push(b' '); if i % 15 == 0 { vec.extend_from_slice(FIZZ); vec.extend_from_slice(BUZZ); } else if i % 3 == 0 { vec.extend_from_slice(FIZZ); } else if i % 5 == 0 { vec.extend_from_slice(BUZZ); } else { vec.extend_from_slice(i.to_string().as_bytes()); } } res }
我试过预先计算字符串容量避免重新分配,也试过直接操作字符串的Vec,但性能几乎没变化。
解答
一、Rust代码的优化方向
1. 消除i.to_string()的堆分配开销
i.to_string()每次都会创建新的String并分配堆内存,这是最大的性能瓶颈。可以用itoa库(栈上格式化数字)替代,完全避免堆分配:
use itoa::Buffer; // 在循环中替换数字格式化部分: let mut buf = Buffer::new(); res.extend_from_slice(buf.format(i).as_bytes());
itoa的Buffer是栈上分配的,格式化速度远快于标准库的to_string()。
2. 精准计算容量,去掉浮点数运算
原calc_capacity用浮点数计算存在误差和性能损耗,改用整数计算精准容量:
fn calc_capacity(n: u32) -> usize { if n <= 1 { return if n == 1 { 1 } else { 0 }; } let mut total = 1; // 初始的'1' let element_count = n - 2; // 2到n-1的元素数量 total += element_count; // 每个元素前的空格 // 统计各类字符串的数量 let count_fizzbuzz = (n - 1) / 15; let count_fizz = ((n - 1) / 3) - count_fizzbuzz; let count_buzz = ((n - 1) / 5) - count_fizzbuzz; let count_numbers = element_count - count_fizzbuzz - count_fizz - count_buzz; // 累加各类字符串的长度 total += count_fizzbuzz * 8; // "fizzbuzz"长度8 total += count_fizz * 4; // "fizz"长度4 total += count_buzz * 4; // "buzz"长度4 // 计算数字的总位数 let mut num_digits = 0; let mut start = 2; for digits in 1..=10 { let end = std::cmp::min(10u32.pow(digits as u32), n); if start >= end { break; } num_digits += (end - start) as usize * digits; start = end; } total += num_digits; total }
3. 避免动态分发的性能损耗
原perf函数用&dyn Fn()导致动态分发,改成泛型函数实现静态分发:
pub fn perf<F: Fn()>(f: F, n: u32) { let start_time = Instant::now(); for _ in 0..n { f(); } let end_time = Instant::now(); println!("{}ms", end_time.duration_since(start_time).as_millis()); }
编译器会内联函数调用,消除动态分发的额外开销。
4. 直接操作字节,跳过Unicode检查
因为所有输出都是ASCII字符,可以直接用Vec<u8>构建结果,最后转成String,避免Rust字符串的Unicode检查:
pub fn fizz_buzz_optimized(n: u32) -> String { let capacity = calc_capacity(n); let mut res = Vec::with_capacity(capacity); res.push(b'1'); for i in 2..n { res.push(b' '); match (i % 3 == 0, i % 5 == 0) { (true, true) => res.extend_from_slice(b"fizzbuzz"), (true, false) => res.extend_from_slice(b"fizz"), (false, true) => res.extend_from_slice(b"buzz"), (false, false) => { let mut buf = itoa::Buffer::new(); res.extend_from_slice(buf.format(i).as_bytes()); } } } String::from_utf8(res).unwrap() }
5. 利用FizzBuzz的周期特性优化循环
FizzBuzz的判断周期是15,可以预计算15个元素的字节序列,然后重复填充,减少循环内的条件判断次数:
const CYCLE: &[u8] = b" 2 3fizz 4buzz fizz 7 8fizz buzz 11fizz 13 14fizzbuzz"; pub fn fizz_buzz_cycle(n: u32) -> String { if n == 1 { return "1".to_string(); } let mut res = Vec::with_capacity(calc_capacity(n)); res.push(b'1'); let full_cycles = (n - 1) / 15; let remainder = (n - 1) % 15; // 填充完整周期 for _ in 0..full_cycles { res.extend_from_slice(CYCLE); } // 填充剩余部分 if remainder > 1 { let end_idx = (remainder - 1) * 2 + match remainder { 3 | 6 | 9 | 12 => 4, 5 | 10 => 4, 15 => 8, _ => 1, }; res.extend_from_slice(&CYCLE[..end_idx as usize]); } String::from_utf8(res).unwrap() }
二、V8引擎的优化机制
1. 即时编译(JIT)与热点代码优化
V8会监控代码执行频率,将被调用1e6次的fizzBuzz标记为热点代码,编译成高度优化的机器码,甚至直接内联到调用循环中,消除函数调用开销。
2. 字符串拼接的特殊优化
V8检测到同一个字符串变量被反复拼接时,会自动使用动态扩容的缓冲区(类似Rust的String),避免每次拼接都重新分配内存,还会根据拼接模式预估算容量。
3. 数字转字符串的快速路径
V8针对整数转字符串做了专门的优化,内置了高效的ASCII转换逻辑,比Rust标准库通用的格式化函数在这个场景下更高效。
4. 逃逸分析与栈分配优化
V8通过逃逸分析判断字符串是否可以在栈上分配(或复用缓冲区),减少堆分配的次数和垃圾回收的开销。
5. 循环优化
V8会对循环做循环展开、条件分支预测优化,比如将i%15这类判断优化为更高效的运算,提升分支预测准确率,减少流水线停顿。
内容的提问来源于stack exchange,提问作者Hoàng

