为什么 NodeJS 在计算素数和方面比 Rust 快?
Why is NodeJS faster than Rust in computing the sum of the primes?
我编写了一个基准测试来计算前 10000 个素数的总和,并将 Rust 与 JavaScript 进行了比较。 JavaNodeJS 上的脚本是 Rust、Scala 和 Java 中最快的。尽管这些程序有意使用函数式风格来测试素数以展示 Rust 的零成本抽象的优势,但 NodeJS 击败了它们。
动态类型运行时 NodeJS 怎么能这么快?
Rust 代码
fn sum_primes(n: usize) -> u64 {
let mut primes = Vec::new();
let mut current: u64 = 2;
let mut sum: u64 = 0;
while primes.len() < n {
if primes.iter().all(|p| current % p != 0) {
sum += current;
primes.push(current);
}
current += 1;
}
sum
}
Java脚本代码
function sumPrimes(n) {
let primes = [];
let current = 2;
let sum = 0;
while (primes.length < n) {
if (primes.every(p => current % p != 0)) {
sum += current;
primes.push(current);
}
++current;
}
return sum;
}
可以找到完整的基准测试 on GitHub。
我认为您的基准测试存在一些缺陷,因为足够先进的编译器甚至可以在编译时(即 Prepack, Closure)将 sum_primes(10000)
优化为 496165411
。也可以在 运行 时间第一次调用后记忆结果,这可能就是 V8 所做的(尽管我希望 HotSpot 做同样的事情)。
使用编译时未知的值代替 10000
,例如命令行参数。
答案并不简单,因为 V8 进行了很多 次转换,但这里有一个重点:
Node 的优化编译器动态调整它使用的类型(尤其是数组元素)。它能够在适合时使用单字整数(并且 取消优化 接收到不适合值时的函数)。
如果我按原样使用你的函数,Rust 需要 1.28 毫秒来计算 sum_prime(500)
,而 Node 只需要 1.04 毫秒(经过一些预热)。如果我将 Rust 代码中的 u64
更改为 u32
,那么它只需要 608µs。
我使用的JavaScript代码:
function sum_primes(n) {
var primes = [];
var current = 2;
var sum = 0;
while (primes.length < n) {
if (primes.every(function (p) { return current % p != 0; })) {
sum += current;
primes.push(current);
}
++current;
}
return sum;
}
console.log(sum_primes(200));
// some warming
for (let i=0; i<100; i++) sum_primes(100);
console.time("primes");
console.log(sum_primes(500));
console.timeEnd("primes");
这个 JavaScript 代码比你的 Rust 代码快,但比这个慢:
use std::time::Instant;
fn sum_primes(n: usize) -> u32 {
let mut primes = Vec::new();
let mut current: u32 = 2;
let mut sum: u32 = 0;
while primes.len() < n {
if primes.iter().all(|p| current % p != 0) {
sum += current;
primes.push(current);
}
current += 1;
}
sum
}
fn main() {
println!("{}", sum_primes(200));
let s = Instant::now();
println!("{}", sum_primes(500));
println!("duration: {:?}", s.elapsed());
}
我编写了一个基准测试来计算前 10000 个素数的总和,并将 Rust 与 JavaScript 进行了比较。 JavaNodeJS 上的脚本是 Rust、Scala 和 Java 中最快的。尽管这些程序有意使用函数式风格来测试素数以展示 Rust 的零成本抽象的优势,但 NodeJS 击败了它们。
动态类型运行时 NodeJS 怎么能这么快?
Rust 代码
fn sum_primes(n: usize) -> u64 {
let mut primes = Vec::new();
let mut current: u64 = 2;
let mut sum: u64 = 0;
while primes.len() < n {
if primes.iter().all(|p| current % p != 0) {
sum += current;
primes.push(current);
}
current += 1;
}
sum
}
Java脚本代码
function sumPrimes(n) {
let primes = [];
let current = 2;
let sum = 0;
while (primes.length < n) {
if (primes.every(p => current % p != 0)) {
sum += current;
primes.push(current);
}
++current;
}
return sum;
}
可以找到完整的基准测试 on GitHub。
我认为您的基准测试存在一些缺陷,因为足够先进的编译器甚至可以在编译时(即 Prepack, Closure)将 sum_primes(10000)
优化为 496165411
。也可以在 运行 时间第一次调用后记忆结果,这可能就是 V8 所做的(尽管我希望 HotSpot 做同样的事情)。
使用编译时未知的值代替 10000
,例如命令行参数。
答案并不简单,因为 V8 进行了很多 次转换,但这里有一个重点:
Node 的优化编译器动态调整它使用的类型(尤其是数组元素)。它能够在适合时使用单字整数(并且 取消优化 接收到不适合值时的函数)。
如果我按原样使用你的函数,Rust 需要 1.28 毫秒来计算 sum_prime(500)
,而 Node 只需要 1.04 毫秒(经过一些预热)。如果我将 Rust 代码中的 u64
更改为 u32
,那么它只需要 608µs。
我使用的JavaScript代码:
function sum_primes(n) {
var primes = [];
var current = 2;
var sum = 0;
while (primes.length < n) {
if (primes.every(function (p) { return current % p != 0; })) {
sum += current;
primes.push(current);
}
++current;
}
return sum;
}
console.log(sum_primes(200));
// some warming
for (let i=0; i<100; i++) sum_primes(100);
console.time("primes");
console.log(sum_primes(500));
console.timeEnd("primes");
这个 JavaScript 代码比你的 Rust 代码快,但比这个慢:
use std::time::Instant;
fn sum_primes(n: usize) -> u32 {
let mut primes = Vec::new();
let mut current: u32 = 2;
let mut sum: u32 = 0;
while primes.len() < n {
if primes.iter().all(|p| current % p != 0) {
sum += current;
primes.push(current);
}
current += 1;
}
sum
}
fn main() {
println!("{}", sum_primes(200));
let s = Instant::now();
println!("{}", sum_primes(500));
println!("duration: {:?}", s.elapsed());
}