如何提高这种算法的性能?

时间:2014-10-01 09:24:03

标签: c# .net performance dictionary parallel-processing

我有一个包含100000对的文本文件:单词和频率。

带有文字的

test.in 文件:

  • 1行 - 所有字频对的总数
  • 2行到~100 001 - 字频对
  • 100 002行 - 用户输入字总数
  • 从100 003到最后 - 用户输入字

我解析这个文件并把文字放在

Dictionary<string,double> dictionary;

我想在以下代码中执行一些搜索+命令逻辑:

for(int i=0;i<15000;i++)
{
    tempInputWord = //take data from file(or other sources)

    var adviceWords = dictionary
                .Where(p => p.Key.StartsWith(searchWord, StringComparison.Ordinal))
                .OrderByDescending(ks => ks.Value)
                .ThenBy(ks => ks.Key,StringComparer.Ordinal)
                .Take(10)
                .ToList();

    //some output
}

问题:此代码必须在不到10秒的时间内运行。

在我的计算机(核心i5 2400,8gb RAM)上使用Parallel.For() - 大约91秒。

您能否就如何提高绩效给我一些建议?

更新:

万岁!我们做到了! 感谢@CodesInChaos,@ usr,@ T_D以及参与解决问题的所有人。

最终代码:

var kvList = dictionary.OrderBy(ks => ks.Key, StringComparer.Ordinal).ToList();

var strComparer = new MyStringComparer();
var intComparer = new MyIntComparer();
var kvListSize = kvList.Count;
var allUserWords = new List<string>();
for (int i = 0; i < userWordQuantity; i++)
{
    var searchWord = Console.ReadLine();
    allUserWords.Add(searchWord);
}
var result =  allUserWords
    .AsParallel()
    .AsOrdered()
    .Select(searchWord =>
    {
        int startIndex = kvList.BinarySearch(new KeyValuePair<string, int>(searchWord, 0), strComparer);
        if (startIndex < 0)
            startIndex = ~startIndex;

        var matches = new List<KeyValuePair<string, int>>();

        bool isNotEnd = true;
        for (int j = startIndex; j < kvListSize ; j++)
        {
            isNotEnd = kvList[j].Key.StartsWith(searchWord, StringComparison.Ordinal);
            if (isNotEnd) matches.Add(kvList[j]);
            else break;
        }
        matches.Sort(intComparer);

        var res = matches.Select(s => s.Key).Take(10).ToList();

        return res;
    });
foreach (var adviceWords in result)
{
   foreach (var adviceWord in adviceWords)
   {
       Console.WriteLine(adviceWord);
   }
   Console.WriteLine();
}
  

6秒(9秒无手动循环(使用linq)))

5 个答案:

答案 0 :(得分:9)

你根本没有使用字典的任何算法强度。理想情况下,您使用树结构,以便您可以执行前缀查找。另一方面,您的性能目标是3.7倍。我认为你可以通过优化算法中的常数因子来实现这一目标。

  1. 不要在执行关键代码中使用LINQ。手动循环遍历所有集合并将结果收集到List<T>。结果证明在实践中可以大大加快速度。
  2. 根本不要使用字典。只需使用KeyValuePair<T1, T2>[]并使用foreach循环运行它。这是遍历一组对的最快方法。
  3. 看起来像这样:

    KeyValuePair<T1, T2>[] items;
    List<KeyValuePair<T1, T2>> matches = new ...(); //Consider pre-sizing this.
    
    //This could be a parallel loop as well.
    //Make sure to not synchronize too much on matches.
    //If there tend to be few matches a lock will be fine.
    foreach (var item in items) {
     if (IsMatch(item)) {
      matches.Add(item);
     }
    }
    
    matches.Sort(...); //Sort in-place
    
    return matches.Take(10); //Maybe matches.RemoveRange(10, matches.Count - 10) is better
    

    这应该超过3.7倍的加速。

    如果您需要更多内容,请尝试将这些项目填充到键入Key的第一个字符的字典中。这样您就可以查找与tempInputWord[0]匹配的所有项目。这应该通过tempInputWord的第一个字符中的选择性来减少搜索时间。对于大约26或52的英文文本。这是前缀查找的原始形式,具有一级查找。不漂亮,但也许就够了。

答案 1 :(得分:4)

我认为最好的方法是使用Trie数据结构而不是字典。 Trie数据结构将所有单词保存在树结构中。节点可以表示以相同字母开头的所有单词。因此,如果您在Trie中查找搜索词 tempInputWord ,您将获得一个表示以 tempInputWord 开头的所有单词的节点,您只需遍历所有子节点。所以你只需要一次搜索操作。维基百科文章的链接还提到了一些优于哈希表的优势(这基本上是一个词典):

  
      
  • 在最糟糕的情况下,在O(m)时间内查找trie中的数据会更快   (其中m是搜索字符串的长度),与不完美相比   哈希表。不完美的哈希表可能存在关键冲突。关键   collision是将不同键的hash函数映射到同一个   在哈希表中的位置。最糟糕的查找速度不完美   哈希表是O(N)时间,但更典型的是O(1),O(m)   花在评估哈希上的时间。
  •   
  • trie中没有不同键的碰撞。
  •   
  • trie中的桶,类似于存储密钥冲突的哈希表桶,只有在单个密钥是   与多个值相关联。
  •   
  • 不需要提供哈希函数或更改哈希函数,因为更多的键被添加到trie中。
  •   
  • 特里可以按键按字母顺序排列。
  •   

here是在C#中创建trie的一些想法。

这至少应该加快查找速度,但是,构建Trie可能会更慢。

<强>更新 好吧,我自己使用频率为英文单词的文件进行了测试,该文件使用与您相同的格式。这是我的代码,它使用了您也尝试使用的Trie类。

    static void Main(string[] args)
    {
        Stopwatch sw = new Stopwatch();

        sw.Start();
        var trie = new Trie<KeyValuePair<string,int>>();

        //build trie with your value pairs
        var lines = File.ReadLines("en.txt");
        foreach(var line in lines.Take(100000))
        {
            var split = line.Split(' ');
            trie.Add(split[0], new KeyValuePair<string,int>(split[0], int.Parse(split[1])));
        }

        Console.WriteLine("Time needed to read file and build Trie with 100000 words: " + sw.Elapsed);

        sw.Reset();

        //test with 10000 search words
        sw.Start();
        foreach (string line in lines.Take(10000))
        {
            var searchWord = line.Split(' ')[0];
            var allPairs = trie.Retrieve(searchWord);
            var bestWords = allPairs.OrderByDescending(kv => kv.Value).ThenBy(kv => kv.Key).Select(kv => kv.Key).Take(10);

            var output = bestWords.Aggregate("", (s1, s2) => s1 + ", " + s2);
            Console.WriteLine(output);

        }

        Console.WriteLine("Time to process 10000 different searchWords: " + sw.Elapsed);
    }

我在非常相似的机器上的结果:

  

读取文件并使用100000字构建Trie所需的时间:00:00:00.7397839
  是时候处理10000种不同的搜索词:00:00:03.0181700

所以我认为你做错了,我们看不到。例如,您测量时间或读取文件的方式。正如我的结果显示这些东西应该非常快。 3秒主要是由于我需要循环中的控制台输出,因此使用了bestWords变量。否则变量就会被优化掉。

答案 2 :(得分:2)

  • List<KeyValuePair<string, decimal>>替换字典,按键排序。

    对于搜索,我使用子字符串在其前缀之前直接排序,并进行序数比较。所以我可以使用二进制搜索来找到第一个候选者。由于候选人是连续的,我可以用Where替换TakeWhile

    int startIndex = dictionary.BinarySearch(searchWord, comparer);
    if(startIndex < 0)
        startIndex = ~startIndex;
    
    var adviceWords = dictionary
                .Skip(startIndex)
                .TakeWhile(p => p.Key.StartsWith(searchWord, StringComparison.Ordinal))
                .OrderByDescending(ks => ks.Value)
                .ThenBy(ks => ks.Key)
                .Select(s => s.Key)
                .Take(10).ToList();
    
  • 确保对所有操作使用序数比较,包括初始排序,二进制搜索和StartsWith检查。

  • 我会在并行循环之外调用Console.ReadLine。可能在搜索字词集合上使用AsParallel().Select(...)而不是Parallel.For

答案 3 :(得分:1)

如果要进行性能分析,请将文件的读取分开并查看所需的时间。 数据计算,收集,演示也可以是不同的步骤。

如果你想要并发和字典,看看ConcurrentDictionary,可能更多的是可靠性而不是性能,但可能两者兼得:

http://msdn.microsoft.com/en-us/library/dd287191(v=vs.110).aspx

答案 4 :(得分:0)

假设10是常数,那为什么每个人都存储整个数据集?记忆不是免费的。最快的解决方案是将前10个条目存储到列表中,然后对其进行排序。然后,在遍历其余数据集时维护10元素排序列表,每次插入元素时都删除第11个元素。

上述方法最适合小值。如果必须使用前5000个对象,请考虑使用二进制堆而不是列表。