【问题标题】:Generic List Performance Optimization通用列表性能优化
【发布时间】:2012-09-26 03:12:16
【问题描述】:

我正在尝试优化通用列表算术运算。我有 3 个可空双精度列表,定义如下。

List<double?> list1 = new List<double?>();
List<double?> list2 = new List<double?>();
List<double?> listResult = new List<double?>();

int recordCount = list1.Count > list2.Count ? list2.Count : list1.Count;

for (int index = 0; index < recordCount; index++)
{
      double? result = list1[index] + list2[index];
      listResult.Add(result);
}

如果我的列表很大,有什么方法可以让这个操作运行得更快?

感谢您的意见。

【问题讨论】:

  • 在将它们添加在一起之前如何填充列表?如果数据来自数据库,那么从 dB 中获取结果的总和会更快。
  • 你知道如果两个列表的大小不同,这段代码会产生 ArrayOutOfBoundsException 吗?
  • @juergend 不,他首先要找到哪个列表更短——第 5 行。
  • @Juergend,此语句“int recordCount = list1.Count > list2.Count ? list2.Count : list1.Count;”会解决问题
  • @jeremy,这些列表是从其他操作中填充的,它不是从数据库中读取的

标签: c# linq generics optimization


【解决方案1】:

如果我的列表很大,有什么方法可以让这个操作运行得更快?

您可以将结果列表创建移动到计数之后:

List<double?> list1 = new List<double?>();
List<double?> list2 = new List<double?>();

int recordCount = list1.Count > list2.Count ? list2.Count : list1.Count;
List<double?> listResult = new List<double?>(recordCount);

这可以让您指定结果所需的确切容量,并避免在列表本身内重新分配。对于“巨大的列表”,这可能是最慢的部分之一,因为随着列表变大,内存分配和复制将是这里最慢的操作。

此外,如果计算简单,您可能会使用多个内核:

List<double?> list1 = new List<double?>();
List<double?> list2 = new List<double?>();

int recordCount = list1.Count > list2.Count ? list2.Count : list1.Count;

var results = new double?[recordCount]; // Use an array here

Parallel.For(0, recordCount, index => 
    {
        double? result = list1[index] + list2[index];
        results[index] = result;
    });

鉴于“工作”在这里非常简单,您可能实际上需要一个自定义分区器来充分利用并行性(详情请参阅How to: Speed Up Small Loop Bodies):

var results = new double?[recordCount]; // Use an array here
var rangePartitioner = Partitioner.Create(0, recordCount);

Parallel.ForEach(rangePartitioner, range => 
    {
        for (int index = range.Item1; index < range.Item2; index++)
        {
            results[index] = list1[index] + list2[index];
        }
    });

但是,如果这不是瓶颈,您可以使用 LINQ 作为单线器来执行此操作:

var results = list1.Zip(list2, (one, two) => one + two).ToList();

但是,如果性能确实是一个瓶颈,这将(非常轻微地)效率低于自己处理循环。

【讨论】:

  • 我很确定 list realloc 会使用某种类型的增长/指数/斐波那契/其他扩展增长系统,所以可能没有你想象的那么糟糕。但我同意只分配一次是一个很好的优化,而且很容易。
  • @mattypiper 确实如此 - 它从 4 个元素开始,每次翻倍。但是,对于非常大的列表,这可能会很昂贵,因为每个 realloc 都需要现有列表的副本。
  • 由于操作如此简单,假设您在具有多个内核、处理器或超线程的系统上运行,并行改进可能最有可能导致性能提高。
  • @CraigSuchanec 是的 - 但是这很简单,它可能需要一个自定义分区器......要编辑
  • @CraigSuchanec 完成 - 如果工作真的这么简单,那实际上会更好。
【解决方案2】:

如果您提前知道列表的大小,那么简单数组应该运行得更快。像这样创建:

double?[] Array1 = new double?[10];

【讨论】:

    【解决方案3】:

    您可以做的第一件事是指定结果列表的容量

    List<double?> listResult = new List<double?>(recordCount);
    

    这将为每个 List.Add() 调用节省时间的结果预先分配空间。

    如果您真的担心性能问题,您可以将列表分成块并启动多个线程来执行部分结果集,然后在完成后将完整集重新合并在一起。

    【讨论】:

      【解决方案4】:
      var result = from i in
                  Enumerable.Range(0, Math.Min(list1.Count, list2.Count))
                  select list1.ElementAtOrDefault(i) + list2.ElementAtOrDefault(i);
      foreach (var item in result)
      {
      Debug.Write(item.Value);
      }
      

      【讨论】:

        【解决方案5】:

        如果您能够使用原始数组而不是列表,那么您当然可以加快速度——具体程度取决于您想要达到的低级别。纠正了您源代码中的一些错误,我写了三个不同的版本。一种方法是通过为结果创建一个新列表(我冒昧地使用具有容量的构造函数,防止一堆中间分配)。

        我还写了一个将两个数组相加为第三个的版本,认为剥离 List 会提高效率。

        最后,我写了一个使用不安全代码的版本,使用指针将第一个数组添加到第二个数组。

        比较结果如下:

        Timings: 500000 iterations of 10000-item lists
          Lists:           00:00:13.9184166
          Arrays (safe):   00:00:08.4868231
          Arrays (unsafe): 00:00:03.0901603
        
        Press any key to continue...
        

        我使用的代码可以在this Github gist找到。

        不安全代码的优化可能有点过多,但看到这三个示例之间的差异还是相当惊人的。为了清楚起见,我建议坚持使用安全代码并使用数组。

        【讨论】:

          猜你喜欢
          • 2015-08-24
          • 2010-12-12
          • 2017-12-18
          • 1970-01-01
          • 1970-01-01
          • 2015-06-12
          • 1970-01-01
          • 2014-10-08
          • 1970-01-01
          相关资源
          最近更新 更多