Eu escrevo alguns pequenos programas que fazem a mesma coisa em diversas linguagens de programação. Comparo o desempenho e o consumo de RAM. Este é um dos programas que escrevi para teste.
Ambos os programas leem o mesmo arquivo. O arquivo contém dados concatenados com os caracteres \t(tab) e \n(enter). O conteúdo é semelhante ao seguinte.
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
O arquivo que criei tem 14 colunas e 63 linhas. Esses números podem mudar. Isso não é importante, pois estou testando.
Obtenho as linhas com split('\n'). E obtenho os campos na linha com split('\t'). Uma desserialização bem simples. O programa lê o arquivo uma vez e o desserializa 200.000 vezes. Em seguida, imprime a hora no console.
Ir:
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)
}
Ferrugem:
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");
}
- vá versão go1.24.2 windows/amd64
- rustc 1.85.1 (4eb161250 2025-03-15)
- SO: Windows 11
Comandos de construção:
go build -ldflags "-s -w"
cargo build --release
Os resultados no meu computador são os seguintes:
Ir:
Time : 4510 milis
RAM usage: 3217 MB
Ferrugem
Time : 5845 milis
RAM usage: 3578 MB
Tentei escrever o código o mais simples possível. Você pode tentar copiando e colando.
O código Rust funciona. Mas é mais lento que o Go e consome mais RAM. Antes de escrever o código, eu esperava que o Rust fosse mais rápido. Talvez haja algo que eu não saiba.
Usar arrays em structs em Rust pode torná-lo mais rápido. Mas não tenho certeza se isso é possível. O que eu quero saber é como posso escrever esse código em Rust para torná-lo mais rápido?
O alocador de sistema no Windows é muito lento. O Rust usa o alocador de sistema por padrão, enquanto o Go tem seu próprio alocador.
Se você substituí-lo, verá que ficará mais rápido. Um alocador para escolher é
mimalloc
. Outra opção possível éjemalloc
, mas é difícil de compilar no Windows.O problema do relógio de parede está em
e
A
Split
é um iterador preguiçoso, portanto tem umsize_hint()
de(0, None)
, portanto a primeira chamada não faz nenhuma pré-alocação eIntoIterator
também não tem especialização fazendo isso para split.A biblioteca padrão do Go conta o número de ocorrências do separador e pré-aloca a fatia resultante em
strings.Split
: https://cs.opensource.google/go/go/+/refs/tags/go1.24.2:src/strings/strings.go;l=298-300Então, em ambos os casos, os vetores serão criados com uma capacidade de 0 e serão redimensionados à medida que forem anexados, e, portanto, a versão Rust está fazendo muito mais alocações do que a versão Go (3 por linha, 5 para a tabela, então 194 por tabela, versus 1 e 1, então 64 por tabela).
Se você pré-alocar os vetores corretamente, estará evitando isso. Na minha máquina (mbp com m1 pro rodando macOS), isso reduz o tempo de execução de 4 para 3 segundos, com o Go em 3,5.
Não realizei nenhuma medição de memória, mas esta também deve ser a fonte da sobrecarga de memória: com a pré-alocação, os buffers terão exatamente 14 itens por linha, e 63 itens por tabela irão para o Rust . Sem o pré-dimensionamento, o fator de crescimento de 2 do Rust significa que a alocação será de 16 itens por linha e 64 por tabela playground .
Isso é uma folga de 2 * 8 * 2 * 63 (2
&str
vezes 63 linhas) + 24 (linha não utilizada) por tabela, vezes 200_000 tabelas, o que é cerca de 390 MB, o que é monitorado.Tente fazer isso sem dividir a contagem.