使用List和Where子句优化C#中的Linq代码

3

我有以下代码:

        var tempResults = new Dictionary<Record, List<Record>>();            
        errors = new List<Record>();
        foreach (Record record in diag)
        {
            var code = Convert.ToInt16(Regex.Split(record.Line, @"\s{1,}")[4], 16);                
            var cond = codes.Where(x => x.Value == code && x.Active).FirstOrDefault();
            if (cond == null)
            {
                errors.Add(record);
                continue;
            }

            var min = record.Datetime.AddSeconds(downDiff);
            var max = record.Datetime.AddSeconds(upDiff);

            //PROBLEM PART - It takes around 4,5ms
            var possibleResults = cas.Where(x => x.Datetime >= min && x.Datetime <= max).ToList();

            if (possibleResults.Count == 0)
                errors.Add(record);
            else
            {                    
                if (!CompareCond(record, possibleResults, cond, ref tempResults, false))
                {                        
                        errors.Add(record);
                }
            }
        }

变量diag是记录列表。

变量cas是记录列表,大约有50k个项目。

问题在于运行速度太慢了。第一个where子句的部分需要大约4,6599毫秒,例如对于List diag中的3000条记录,它需要3000*4,6599=14秒。是否有任何优化代码的选项?


2
你可以在forEach循环之外预先过滤codes,只保留活动的项目。这样可以减少在循环内部需要搜索的数量。 - Marco
是的,没错,谢谢您提醒。我已经完成了。现在它好了一些,但主要问题仍然是带有时间限制的where子句。 - Filip Procházka
3个回答

7
您可以加速您强调的特定语句。
cas.Where(x => x.Datetime >= min && x.Datetime <= max).ToList();

使用二分查找在cas列表中。首先按Datetimecas进行预排序:

cas.Sort((a,b) => a.Datetime.CompareTo(b.Datetime));

然后创建一个Record的比较器,只比较Datetime属性(实现假定列表中没有空记录):

private class RecordDateComparer : IComparer<Record> {
    public int Compare(Record x, Record y) {
        return x.Datetime.CompareTo(y.Datetime);
    }
}

那么您可以这样翻译 Where 子句:

var index = cas.BinarySearch(new Record { Datetime = min }, new RecordDateComparer());
if (index < 0)
    index = ~index;
var possibleResults = new List<Record>();    
// go backwards, for duplicates            
for (int i = index - 1; i >= 0; i--) {
    var res = cas[i];
    if (res.Datetime <= max && res.Datetime >= min)
        possibleResults.Add(res);
    else break;
}
// go forward until item bigger than max is found
for (int i = index; i < cas.Count; i++) {
    var res = cas[i];
    if (res.Datetime <= max &&res.Datetime >= min)
        possibleResults.Add(res);
    else break;
}    

想法是使用二分搜索找到第一个日期等于或大于您的最小日期的记录。如果找到完全匹配,则返回匹配元素的索引。如果未找到,则返回负值,可以通过 ~index 操作将其转换为大于目标的第一个元素的索引。
当我们找到该元素时,我们只需向列表前进并获取项,直到我们找到大于最大值的 Datetime 的项(因为列表已排序)。我们还需要稍微向后退一下,因为如果有重复项-二分搜索不一定返回第一个,因此我们需要向后退以查找潜在的重复项。
其他改进可能包括:
- 在 for 循环之外使用字典(以 Value 为键)放置活动代码,从而用 Dictionary.ContainsKey 替换代码 Where 搜索。 - 如 @Digitalsa1nt 的评论所建议的那样-使用 Parallel.For、PLINQ 或任何类似的技术并行化 foreach 循环。这是并行化的完美案例,因为循环仅包含 CPU 绑定的工作。当然,您需要进行一些调整以使其线程安全,例如使用线程安全集合来处理错误(或锁定添加到其中的操作)。

谢谢!太好了。执行时间从4.5毫秒减少到了约0.5毫秒,效果更好了。这是我第一次听说二分查找,很好知道它。 :) - Filip Procházka
@FilipProcházka 我有一种感觉这个代码可以更快,但是如果没有代码的目标,我无法提供更多的指导。但是你可以将活动代码移出循环并放入'HashSet'中。如果你有很多代码,这可能会稍微加速一些。但是除此之外,我没有更多的想法了。 - Evk
@Evk,您是否考虑在这里实现PLinq以并行执行任务,或者这是否不可能? - JoeTomks
1
@Digitalsa1nt 是的,实际上这是并行化的完美案例。几乎没有可变共享状态,CPU 绑定工作。 - Evk
@FilipProcházka 上面有一个很好的建议 - 你可以通过并行化循环来进一步提高速度。 - Evk
显示剩余3条评论

1
尝试在列表中添加AsNoTrackingAsNoTracking 方法可以节省执行时间和内存使用。当从数据库检索大量数据时,应用此选项确实变得很重要。
var possibleResults = cas.Where(x => x.Datetime >= min && x.Datetime <= max).AsNoTracking().ToList(); //around 4,6599ms

很遗憾,它没有任何效果。 - Filip Procházka

0

这里有一些改进的空间。

虽然只是轻微的性能提升,但在这种情况下,你应该尝试使用 groupby 而不是 where。

因此,你应该像这样:

cas.GroupBy(x => x.DateTime >= min && x.DateTime <= max).Select(h => h.Key == true);

通常这种方法适用于在列表中搜索不同的值,但是在您的情况下,我不确定当使用子句时它是否会为您提供任何好处。

此外,在您的代码中还有其他一些事情可以做:

  • 尽可能避免使用ToList,而是坚持使用IEnumerable。ToList执行急切评估,这可能导致查询速度变慢。
  • 在检查值是否存在时,请使用.Any()而不是Count(仅适用于列表为IEnumerable的情况)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接