我用多种编程语言编写了一些实现相同功能的小程序。我比较了它们的性能和内存占用。这是我编写的测试程序之一。
两个程序读取同一个文件。该文件包含由 \t(制表符)和 \n(回车符)连接的数据。内容类似如下。
aaaa\tbbb\tccc\tddd\teee\tfff\tggg\thhh\n
aaaa\tbbb\tccc\tddd\teee\tfff\tggg\thhh\n
aaaa\tbbb\tccc\tddd\teee\tfff\tggg\thhh\n
aaaa\tbbb\tccc\tddd\teee\tfff\tggg\thhh
我创建的文件有 14 列和 63 行。这些数字可能会更改。这并不重要,因为我正在测试它。
我使用 split('\n') 获取行。然后使用 split('\t') 获取行中的字段。这是一个非常简单的反序列化过程。程序读取文件一次,然后对其进行 200,000 次反序列化。然后将时间打印到控制台。
去:
package main
import (
"fmt"
"os"
"strings"
"time"
)
type Datatable struct {
id int
rows [][]string
}
func main() {
start := time.Now()
dat, err := os.ReadFile("C:\\Temp\\test1.txt")
if err != nil {
panic("file not found")
}
str := string(dat)
count := 200_000
tables := make([]Datatable, count)
for i := 0; i < count; i++ {
table := Datatable{i, nil}
var lines []string = strings.Split(str, "\n")
table.rows = make([][]string, len(lines))
for j, l := range lines {
table.rows[j] = strings.Split(l, "\t")
}
tables[i] = table
}
end := time.Now()
elapsed := end.Sub(start)
fmt.Println("Time: ", elapsed)
var b []byte = make([]byte, 1)
os.Stdin.Read(b)
}
锈:
use std::fs;
use std::time::SystemTime;
use std::io::{self, BufRead};
struct Table<'a>{
id: usize,
rows: Vec<Vec<&'a str>>,
}
fn main() {
let start = SystemTime::now();
let str = fs::read_to_string("C:\\Temp\\test1.txt")
.expect("read_to_string: failed");
let count = 200_000;
let mut tables = Vec::with_capacity(count);
for i in 0..count {
let lines = str.split('\n');
let mut table = Table {
id : i,
rows : Vec::with_capacity(lines.size_hint().0),
};
for item in lines {
table.rows.push(item.split('\t').collect::<Vec<&str>>());
}
tables.push(table);
}
println!("Time: {}", start.elapsed().expect("elapsed: failed").as_millis());
let mut line = String::new();
let stdin = io::stdin();
stdin.lock().read_line(&mut line).expect("read_line: failed");
}
- go版本go1.24.2 windows/amd64
- rustc 1.85.1 (4eb161250 2025-03-15)
- 操作系统:Windows 11
构建命令:
go build -ldflags "-s -w"
cargo build --release
在我的电脑上结果如下:
去:
Time : 4510 milis
RAM usage: 3217 MB
锈
Time : 5845 milis
RAM usage: 3578 MB
我尽量把代码写得简单一点,大家可以直接复制粘贴试试。
Rust 代码可以运行。但它比 Go 慢,而且占用更多内存。在编写代码之前,我希望 Rust 能跑得更快。也许有什么我不知道的地方。
在 Rust 的结构体中使用数组可能会使其运行得更快。但我不确定这是否可行。我想知道的是,如何用 Rust 编写这段代码才能使其运行得更快?
Windows 上的系统分配器非常慢。Rust 默认使用系统分配器,而 Go 有自己的分配器。
如果你替换它,你会发现它会更快。一个可选的分配器是
mimalloc
。另一个可能的选项是jemalloc
,但它在 Windows 上构建起来很困难。挂钟问题在于
和
A
Split
是一个惰性迭代器,所以它具有size_hint()
,(0, None)
因此第一次调用不会进行任何预分配,并且IntoIterator
也没有针对 split 执行此操作的专门化。Go stdlib 确实会计算分隔符出现的次数,并预先分配生成的切片
strings.Split
:https://cs.opensource.google/go/go/+/refs/tags/go1.24.2 :src/strings/strings.go;l=298-300因此,在这两种情况下,向量的容量都是 0,并且在追加时会调整大小,因此 Rust 版本比 Go 版本分配更多(每行 3 个,每个表 5 个,所以每个表 194 个,而 Go 版本为 1 个和 1 个,所以每个表 64 个)。
如果你正确地预分配向量,就可以避免这种情况。在我的机器上(运行 macOS 的 mbp 和 m1 pro),这将运行时间从 4 秒缩短到 3 秒,Go 的运行时间为 3.5 秒。
我没有做过任何内存测量,但这也应该是内存开销的来源:预分配后,缓冲区的大小将精确到每行 14 个项目,每个表go rust 63 个项目。如果不进行预分配,Rust 的增长因子为 2,意味着每行分配 16 个项目,每个表playground分配 64 个项目。
这是每个表的 2 * 8 * 2 * 63(2 乘以 63 行)+ 24(未使用的行)的冗余量
&str
,乘以 200_000 个表,大约是 390MB。通过不分割计数来尝试此操作。