【问题标题】:Hashmap slower than string.find?Hashmap 比 string.find 慢?
【发布时间】:2021-11-30 15:39:17
【问题描述】:

我在 leetcode 中做练习,作为学习 Rust 的一种方式。一个练习涉及找到最长的子字符串,而字符串中没有任何字符重复。

我的第一个想法是将子字符串存储在字符串中并搜索字符串以查看字符是否已在其中:

impl Solution {
    pub fn length_of_longest_substring(s: String) -> i32 {
        let mut unique_str = String::from("");
        let mut schars: Vec<char> = s.chars().collect();   
        let mut longest = 0 as i32;
        for x in 0..schars.len()
        {
            unique_str = schars[x].to_string(); 
            for y in x+1..schars.len()
            {            
                if is_new_char(&unique_str, schars[y])
                {
                    unique_str.push(schars[y]);
                } else {
                    break;
                }
            }
            let cur_len = unique_str.len() as i32;
            if cur_len > longest {
                longest = cur_len;
            }
        }
        longest
    }
}

fn is_new_char ( unique_str: &str, c: char ) -> bool {
    if unique_str.find(c) == None
    {
        true
    } else {
        false
    }                
}

它工作正常,但性能偏低。希望在“查找”操作上减少几毫秒,我将 unique_str 替换为 HashMap:

use std::collections::HashMap;
impl Solution {
    pub fn length_of_longest_substring(s: String) -> i32 {
        let mut hash_str = HashMap::new();
        let mut schars: Vec<char> = s.chars().collect(); 
        let mut longest = 0 as i32;
        for x in 0..schars.len()
        {            
            hash_str.insert(schars[x], x);
            for y in x+1..schars.len()
            {            
                if hash_str.contains_key(&schars[y]){
                    break;
                } else {
                    hash_str.insert(schars[y], y); 
                }
            }
            let cur_len = hash_str.len() as i32; 
            if cur_len > longest {
                longest = cur_len;
            }
            hash_str.clear();
        }
        longest
    }
}

令人惊讶的是,String.find() 版本在基准测试中比 HashMap 快 3 倍,尽管我使用的是相同的算法(或者至少我是这么认为的)。直觉上,我会假设在 hashmap 中进行查找应该比搜索字符串的字符要快得多,但结果恰恰相反。

有人能解释一下为什么 HashMap 这么慢吗? (或指出我做错了什么)。

【问题讨论】:

  • 你的琴弦有多大?
  • 不是 O(n) 可以解决的问题吗?
  • O(1) 算法(散列查找)可能比 O(n) 算法(线性搜索)慢,但对实际数据没有任何特殊了解,可能有多种原因,这主要是猜测。此实现存在一些问题,例如每个 hashmap 条目包含与键和值相同的内容,但这可能无关紧要。 HashMap 默认使用慢速(加密)散列器;您可以通过将其换成快速的来做得更好。
  • 请注意,问题不仅与输入字符串中的字符数有关,还与输入流中的 distinct 字符数有关,所以如果 leetcode 有一堆ababaabacabbcaabccababc 形式的测试无论是 map 还是 Vec 都不会比 n=3 长,这是很小的。但这又只是猜测。
  • 另一件事是 rust 使用了一个相对较慢但哈希 dos 抗性的哈希函数。使用针对小型输入(例如 fnv)优化的哈希函数应该会快得多。

标签: performance rust hashmap


【解决方案1】:

在性能方面,一项测试总是优于 10 个原因。

use std::hash::{Hash, Hasher};

fn main() {
    let start = std::time::SystemTime::now();
    let mut hasher = std::collections::hash_map::DefaultHasher::new();

    let s = "a";
    let string = "ab";
    for i in 0..100000000 {
         s.hash(&mut hasher);
         let hash = hasher.finish();
    }

    eprintln!("{}", start.elapsed().unwrap().as_millis());
}

我使用调试构建,因此编译器不会优化我的大部分代码。

在我的机器上,上面的 100M 哈希需要 14 秒。如果我按照某些 cmets 的建议将 DefaultHasher 替换为 SipHasher,则需要 17 秒。

现在,字符串变体:

use std::hash::{Hash, Hasher};

fn main() {
    let start = std::time::SystemTime::now();

    let string = "abcde";
    for i in 0..100000000 {
        for c in string.chars() {
            // do nothing
        }
    }

    eprintln!("{}", start.elapsed().unwrap().as_millis());
}

使用字符串中的 5 个字符执行此代码需要 24 秒。如果有 2 个字符,则需要 12 秒。

现在,它如何回答你的问题?..

要将值插入哈希集中,必须计算哈希。那么每次要检查一个字符是否在hashset中,都需要重新计算一个hash。与仅计算哈希相比,检查值是否在哈希集中也有一些小的开销。

从测试中我们可以看出,计算单个字符串的一个哈希值与迭代 3 个符号字符串所花费的时间大致相同。因此,假设您有一个 unique_str,其值为 abcde,然后检查其中是否有 x 字符。使用HashSet 进行检查会更快,但是您还需要将x 添加到集合中,这使得它需要2 个哈希值来对抗迭代5 符号字符串。

因此,只要您的 unique_str 平均少于 5 个符号,就可以保证字符串实现更快。如果是 aaaaaaaaa.... 这样的输入字符串,它会快 6 倍,然后是 HashSet 选项。

当然,这个分析非常简单,可能还有许多其他因素在起作用(如编译器优化和字符串的哈希和查找的具体实现),但它给出了想法,为什么在某些情况下HashSet 可能会更慢然后string.find()

旁注:在您的代码中,您使用HashMap 而不是HashSet,这会增加更多开销,并且在您的情况下不需要...

【讨论】:

  • “我使用调试构建,因此编译器不会优化我的大部分代码” - 你应该永远对未优化的代码进行基准测试。它有太多问题:一方面,您没有运行最终将在该领域使用的东西;第二,应该是零成本的东西最终可能会产生成本;三、看似很小的改动,优化后却能产生很大的不同。有一些方法可以解决由过度渴望的优化器引起的常量折叠问题。考虑使用 criterion 之类的东西来为你处理这个以及更多的事情。
  • never 是一个非常强烈的词,我不同意你的看法。但是感谢您提供标准链接。
  • 是的,never 不是一个强词,我使用它是因为我觉得它很合适。未优化的基准测试只告诉您未优化代码的性能,这通常不是您关心的。由于我上面列出的原因以及更多原因,认为您可以基于它推测优化版本的性能是幼稚的。请不要根据不良数据做出决定。
猜你喜欢
  • 2016-11-21
  • 2013-03-23
  • 2015-11-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-14
  • 1970-01-01
  • 2017-08-08
相关资源
最近更新 更多