为什么 Rust 中的这段嘶嘶声代码比 Node.js 慢?

问题描述 投票:0回答:1

我编写了一个 Rust 函数,它接受 n 并返回从 1 到 n 的嘶嘶声序列。然后我用 JavaScript 编写了等效的代码并做了一些基准测试。 Node.js 比 Rust 快得多。我确实使用

cargo run -r
在发布模式下编译并运行 Rust。

对于 n=100,重复次数=1e6:
Node.js:707ms
铁锈:1047ms

对于 n=1000,重复次数=1e6:
Node.js:6.7 秒
生锈: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

生锈

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());
}

perf(
    &|| {
        fizz_buzz(100);
    },
    1e6 as u32,
);
perf(
    &|| {
        fizz_buzz(1000);
    },
    1e6 as u32,
);
// 1047ms
// 10927ms

我尝试通过提前计算结果字符串的容量来优化 Rust 代码以防止重新分配,并尝试直接使用字符串的 vec,但结果完全相同。

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
}
node.js performance rust
1个回答
0
投票

JavaScript 在这里有一个很大的优势:它有一个垃圾收集器。在这段代码中,我们分配了很多小对象并释放它们(对于数字的字符串)。在 V8 中,分配只是指针碰撞,而释放则是集体完成的;在 Rust 中,我们使用完整的分配器。

幸运的是,Rust 比 JS 更底层,我们可以通过使用堆栈分配数组来避免创建垃圾:

pub fn v2(n: u32) -> String {
    let mut numbers_buffer = [0; 20];
    let mut res = Vec::with_capacity(calc_capacity(n as f32) as usize);
    res.push(b'1');
    for mut i in 2..n {
        res.push(b' ');
        if i % 15 == 0 {
            res.extend_from_slice(b"fizzbuzz");
        } else if i % 3 == 0 {
            res.extend_from_slice(b"fizz");
        } else if i % 5 == 0 {
            res.extend_from_slice(b"buzz");
        } else {
            let mut current_buffer_position = 19;
            loop {
                numbers_buffer[current_buffer_position] = (i % 10) as u8 + b'0';
                i /= 10;
                if i == 0 {
                    break;
                }
                current_buffer_position -= 1;
            }

            res.extend_from_slice(&numbers_buffer[current_buffer_position..]);
        }
    }
    String::from_utf8(res).unwrap()
}

完全不安全,这段代码在我的机器上运行时间为 295 毫秒和 2.851 秒,而你的 Rust 代码运行时间为 1.669 秒和 17.165 秒,你的 JS 代码运行时间为 642 毫秒和 5.926 秒。

我相信 V8 这里还有另一个技巧:它并没有真正分配和复制字符串以在此处增长它,它只是在内存中将其表示为连接字符串的树。但这还不足以击败我们优化的 Rust,尽管它并没有做到这一点。

© www.soinside.com 2019 - 2024. All rights reserved.