You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何Rust版FizzBuzz代码比Node.js慢?如何优化?

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

想请教两个问题:

  1. 怎么改写Rust代码提升性能?
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 14:47:08