我使用以下两段代码对 Varint 进行编码,NodeJS 大约需要 900ms,而 Rust 大约需要 2700ms。为什么性能差距这么大?
看起来分配内存比较耗时,NodeJS 对于分配内存还有其他优化吗?
NodeJS版本:v20.10.0
Rust 版本:1.75.0
JavaScript 代码:
// node index.js
const { performance } = require('node:perf_hooks')
function encode_varint(num) {
let buf_size = 0;
let cmp_number = num;
while (cmp_number) {
cmp_number >>>= 7
buf_size += 1;
}
const buf = new Array(buf_size)
let temp_num = num;
let index = 0;
while (temp_num > 0) {
if ((temp_num >>> 7) !== 0) {
buf[index++] = 0x80 | (temp_num & 0x7f)
temp_num >>>= 7;
} else {
buf[index++] = temp_num & 0x7f
break
}
}
return buf;
}
const N = 100_000_000;
const start = performance.now();
for (let i = 0; i < N; i++) {
const num = 999_999_999_999_999;
const buf = encode_varint(num);
// console.log(buf)
}
const end = performance.now();
const elapsed = end - start;
console.log(elapsed);
Rust 代码:
// cargo r --release
use std::time;
fn encode_varint(num: usize) -> Vec<u8> {
let buf_size = {
let mut size = 1;
let mut cmp_number = num;
while cmp_number > 0 {
cmp_number >>= 7;
size += 1;
}
size
};
let mut buf: Vec<u8> = Vec::with_capacity(buf_size);
let mut temp_num = num;
while temp_num > 0 {
if (temp_num >> 7) != 0 {
buf.push((0x80 | (temp_num & 0x7f)) as u8);
temp_num >>= 7;
} else {
buf.push((temp_num & 0x7f) as u8);
break;
}
}
buf
}
const N: u32 = 100_000_000;
fn main() {
let start = time::Instant::now();
let num = 999_999_999_999_999;
for _ in 0..N {
let _buf = encode_varint(num);
// dbg!(_buf);
}
let end = time::Instant::now();
let elapsed = end - start;
println!("{}", elapsed.as_millis());
}
两个程序的计算不一样。 通过
999_999_999_999_999
,我们在 Rust 中获得 [255, 255, 153, 166, 234, 175, 227, 1]
,而在 JS 中我们获得 [ 255, 255, 153, 166, 10 ]
。
这是因为在 JS 中我们无法一次性选择数值表达式的确切类型;它根据操作而变化。 例如,初始值很大,但没有损失地位于双精度浮点的尾数中。 操作
>>> 7
,与 JS 中的所有按位操作一样,强制转换为 32 位整数(这在文档中有描述,这是 WebAssembly 背后的原始想法,导致了 Wasm),然后截断在 JS 中会发生这种情况,而在 Rust 中则不会。
例如,如果我们在 JS 中每次迭代开始时打印 temp_num >>> 7
的结果,我们会看到
NEXT 21597439
NEXT 168729
NEXT 1318
NEXT 10
NEXT 0
在 Rust 中我们看到
NEXT 7812499999999
NEXT 61035156249
NEXT 476837158
NEXT 3725290
NEXT 29103
NEXT 227
NEXT 1
NEXT 0
从这里开始,我们无法比较两个版本的性能,因为它们并不等同。
另一方面,正如评论中所述,由于这个微基准的人为重复形状,JS 中的分配策略可能更好(但在实际问题上仍然如此吗?)。
cargo flamegraph
证实了这一点,因为大部分时间都花在分配/释放上。
为了提高性能,我们可以使函数清晰并始终改变相同的向量(作为 &mut
传递,在 main()
中一次性创建),而不是在每次迭代时构建一个新向量(这也会删除需要计算 buf_size
)。