You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust代码性能远劣于Go的原因排查求助

Rust代码性能远低于Go版本的问题求助

我知道这不是这里的常规问题,但还是希望有人能帮我分析这段Rust代码性能极差的原因——所用词典约有1400个词,我的Go版本代码仅需约5分钟运行完成,而Rust版本却耗时约100分钟。我最初在dictionary_attack函数中有不少.clone()调用,已将其移除;还将部分.unwrap()替换为?运算符和.expect(),原以为这是性能瓶颈,但问题仍未解决。以下是完整的Rust代码:

use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
use std::fs::File;
use std::io::{BufRead, BufReader};

#[allow(dead_code)]

struct Key {
    a: [char; 25],
    b: [char; 25],
    c: [char; 25],
    d: [char; 25],
}

struct Coords {
    row: i32,
    column: i32,
}

fn letters() -> [char; 25] {
    let l: [char; 25] = [
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
        'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
    ];
    l
}

fn format_text(original: String) -> String {
    let mut result: String = "".to_string();
    for i in 0..original.len() {
        if original.as_bytes()[i] >= 65 && original.as_bytes()[i] <= 90 {
            result.push(original.as_bytes()[i] as char)
        }
    }
    result
}

fn scan_tetragrams() -> HashMap<String, i32> {
    let mut tg: HashMap<String, i32> = HashMap::new();
    let file = File::open("../tetragrams.txt").expect("Failed to load tetragram file");
    let reader = BufReader::new(&file);
    for line in reader.lines() {
        let ln: &str = &line.unwrap_or(String::from("Invalid line"));
        let fields: Vec<&str> = ln.split(", ").collect();
        let n: Result<i32, _> = fields[1].parse();
        let num: i32 = match n {
            Ok(n) => n,
            Err(_) => panic!("Failed to convert tetragram frequence to i32"),
        };
        tg.insert(fields[0].to_string(), num);
    }
    tg
}

fn gen_reference(n: usize) -> String {
    let r: Vec<u8> = format_text(read("../referenceText.txt"))
        .as_bytes()
        .to_vec();
    let mut ref_text: String = String::from("");
    for i in 0..n {
        ref_text = format!("{}{}", ref_text, String::from(r[i] as char));
    }
    ref_text
}

fn scan_dict() -> Vec<String> {
    let words: Vec<String> = read("../challenge_word_list.txt")
        .split(" ")
        .collect::<Vec<&str>>()
        .iter()
        .map(|&s| s.into())
        .collect(); //this was painful lmao
    words[..words.len() - 1].to_vec() //last element is a newline character otherwise
}

fn read(path: &str) -> String {
    let f: Result<String, std::io::Error> = fs::read_to_string(path);
    match f {
        Ok(s) => s,
        Err(_) => panic!("Couldn't find file"),
    }
}

fn score(text: &String, tetragrams: &HashMap<String, i32>) -> i32 {
    let bytes = text.as_bytes();
    let mut total: i32 = 0;
    for i in 0..text.len() - 3 {
        let v: Vec<u8> = (bytes[i..i + 4]).to_vec();
        let s: Result<String, _> = String::from_utf8(v);
        let slice = match s {
            Ok(s) => s,
            Err(_) => panic!("Failed to parse window in text"),
        };
        if tetragrams.contains_key(&slice) {
            total += tetragrams[&slice];
        }
    }
    total
}

fn fill_grid(keyword: &mut String) -> [char; 25] {
    let mut seen: HashSet<char> = HashSet::new();
    keyword.retain(|c| {
        let s = seen.contains(&c);
        seen.insert(c);
        !s
    }); //remove duplicate letters from keyword

    let last_idx = letters()
        .iter()
        .position(|&x| x == keyword.chars().last().unwrap())
        .unwrap(); //continues filling up grid with next letter after last letter in keyword
    let mut grid: Vec<char> = keyword
        .as_bytes()
        .to_vec()
        .iter()
        .map(|x| *x as char)
        .collect(); //also painful
    for i in last_idx..last_idx + 25 {
        let idx = i % 25;
        if !seen.contains(&letters()[idx]) {
            seen.insert(letters()[idx]);
            grid.push(letters()[idx]);
        }
    }
    let t: Result<[char; 25], _> = grid.try_into();
    match t {
        Ok(arr) => arr,
        Err(_) => panic!("Failed to fill grid"),
    }
}

fn coordinates(grid_index: i32) -> Coords {
    let row: i32 = grid_index / 5;
    let column: i32 = grid_index - row * 5;
    Coords { row, column }
}

fn grid_index(row: i32, column: i32) -> i32 {
    row * 5 + column
}

fn decipher(text: &String, k: Key) -> Option<String> {
    let mut deciphered: String = String::from("");
    let bytes = text.as_bytes();
    for i in (0..text.len() - 1).step_by(2) {
        let v: Vec<u8> = (bytes[i..i + 2]).to_vec();
        let i1: usize = k.b.iter().position(|&x| x == v[0] as char)?;
        let i2: usize = k.c.iter().position(|&x| x == v[1] as char)?;
        let coords1: Coords = coordinates(i1.try_into().ok()?);
        let coords2: Coords = coordinates(i2.try_into().ok()?);

        let d_i1: usize = grid_index(coords1.row, coords2.column)
            .try_into()
            .expect("Failed to get grid index 1"); //coords of deciphered letters
        let d_i2: usize = grid_index(coords2.row, coords1.column)
            .try_into()
            .expect("Failed to get grid index 2");

        let letter1: String = String::from(k.a[d_i1]);
        let letter2: String = String::from(k.d[d_i2]);
        deciphered = format!("{}{}{}", deciphered, letter1, letter2);
    }
    Some(deciphered)
}

fn dictionary_attack(ciphertext: &String) {
    let dict = scan_dict();
    let tetragrams = scan_tetragrams();
    let ref_score = score(&gen_reference(ciphertext.clone().len()), &tetragrams);
    let mut count: i32 = 0;
    for word in &dict {
        println!("{}", count);
        count += 1;
        if word.contains("J") || word.len() == 1 {
            continue;
        }
        for word2 in &dict {
            if word2.contains("J") || word2.len() == 1 {
                continue;
            }
            let k: Key = Key {
                a: letters(),
                b: fill_grid(&mut word.to_string()),
                c: fill_grid(&mut word2.to_string()),
                d: letters(),
            };
            let p = decipher(&ciphertext, k).unwrap();
            if score(&p, &tetragrams) * 10 >= ref_score * 8 {
                println!("{}", p)
            }
        }
    }
}

fn main() {
    let original = read("src/ciphertext.txt");
    let ciphertext: String = format_text(original);
    dictionary_attack(&ciphertext)
}

核心性能瓶颈分析

1. 字符串拼接的低效操作

  • gen_reference中循环使用format!拼接字符串,每次都会创建新的String,触发频繁内存分配与拷贝。应该改用String::push,或者预先用with_capacity分配足够容量的字符串,避免扩容开销。
  • decipher中同样用format!拼接解密结果,对于较长密文来说,每次循环的内存分配会累积成巨大开销,同样建议改用push直接追加字符。

2. score函数的冗余内存分配

  • 每次循环都将四元组字节切片拷贝成Vec<u8>再转成String,完全没必要。原文本是合法的大写ASCII字母,直接取&text[i..i+4]作为字符串切片,就能直接查询HashMap,省去拷贝和内存分配。

3. 线性查找的巨大开销

  • decipher中用iter().position()在字符数组中查找位置,每次都是O(n)的线性遍历。对于1400*1400次嵌套循环,每次解密还要多次执行这种查找,累计开销极大。建议为每个网格数组预先创建反向映射(比如[usize; 26],因为字母是固定的A-Z,J可以跳过),将查找变成O(1)的直接索引。

4. fill_grid的重复计算与冗余操作

  • 同一个单词会在嵌套循环中被多次调用fill_grid,可以用HashMap<String, [char;25]>缓存每个单词对应的网格结果,避免重复计算。
  • 每次调用letters()都会重新创建数组,可将其定义为全局常量,避免重复初始化。
  • fill_grid中修改临时字符串的操作可以优化:无需传入可变引用,直接对单词的字符去重后生成新的序列即可,避免不必要的字符串拷贝。

5. 其他细节优化

  • dictionary_attack中ciphertext.clone().len()完全没必要克隆字符串,直接用ciphertext.len()即可。
  • scan_dict中分割字符串后手动去掉最后一个元素的做法不够稳健,应该在收集时过滤掉空字符串和换行符,比如用.filter(|s| !s.is_empty() && !s.contains('\n'))。

内容的提问来源于stack exchange,提问作者all_caps

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 02:50:57