为什么数组项目分配会降低C#程序的性能?

时间:2013-11-05 20:07:12

标签: c# .net arrays performance

摘要

在处理大型文本文件时,我遇到了以下(意外)性能下降,我无法解释。我对这个问题的目标是:

  • 了解导致下面所述减速的原因
  • 了解如何快速初始化大型非原始数组

问题

  • 数组包含非基本参考项
    • 不是int[],而是MyComplexType[]
    • MyComplexType是一个类,而不是结构
    • MyComplexType包含一些string属性
  • 预先分配数组
  • 数组很大
  • 如果在未分配给阵列的情况下创建和使用某个项目,则程序很快
  • 如果已创建项目,然后已分配给数组项,则程序会显着减慢
    • 我希望数组项目分配是一个简单的引用分配,但这似乎不是基于我的测试程序下面的结果

测试程序

考虑以下C#计划:

namespace Test
{
    public static class Program
    {
        // Simple data structure
        private sealed class Item
        {
            public Item(int i)
            {
                this.Name = "Hello " + i;
                //this.Name = "Hello";
                //this.Name = null;
            }
            public readonly string Name;
        }

        // Test program
        public static void Main()
        {
            const int length = 1000000;
            var items = new Item[length];

            // Create one million items but don't assign to array
            var w = System.Diagnostics.Stopwatch.StartNew();
            for (var i = 0; i < length; i++)
            {
                var item = new Item(i);
                if (!string.IsNullOrEmpty(item.Name)) // reference the item and its Name property
                {
                    items[i] = null; // do not remember the item
                }
            }
            System.Console.Error.WriteLine("Without assignment: " + w.Elapsed);

            // Create one million items and assign to array
            w.Restart();
            for (var i = 0; i < length; i++)
            {
                var item = new Item(i);
                if (!string.IsNullOrEmpty(item.Name)) // reference the item and its Name property
                {
                    items[i] = item; // remember the item
                }
            }
            System.Console.Error.WriteLine("   With assignment: " + w.Elapsed);
        }
    }
}

它包含两个几乎相同的循环。每个循环创建一百万个Item类实例。第一个循环使用创建的项目,然后抛弃引用(不将其保留在items数组中)。第二个循环使用创建的项目,然后将引用存储在items数组中。数组项目分配是循环之间的唯一区别。

我的结果

  • 当我在我的机器上运行Release版本(已启用优化)时,我得到以下结果:

    Without assignment: 00:00:00.2193348
       With assignment: 00:00:00.8819170
    

    具有数组赋值的循环明显慢于没有赋值的循环(慢4倍)。

  • 如果我更改Item构造函数以将常量字符串赋给Name属性:

    public Item(int i)
    {
        //this.Name = "Hello " + i;
        this.Name = "Hello";
        //this.Name = null;
    }
    

    我得到以下结果:

    Without assignment: 00:00:00.0228067
       With assignment: 00:00:00.0718317
    

    带有赋值的循环仍然比没有

  • 的循环慢约3倍
  • 最后,如果我将null分配给Name属性:

    public Item(int i)
    {
        //this.Name = "Hello " + i;
        //this.Name = "Hello";
        this.Name = null;
    }
    

    我得到以下结果:

    Without assignment: 00:00:00.0146696
       With assignment: 00:00:00.0105369
    

    一旦没有分配字符串,没有赋值的版本最终会稍微慢一点(我假设因为所有这些实例都是为垃圾收集而发布的)

问题

  • 为什么数组项目分配对测试程序的影响如此之大?

  • 是否有一个属性/语言构造/等会加快作业的速度?

PS:我尝试使用dotTrace调查减速,但它没有结果。我看到的一件事是循环中的字符串复制和垃圾收集开销要多于没有赋值的循环(即使我预期相反)。

6 个答案:

答案 0 :(得分:26)

我怀疑大多数时间问题与内存分配有关。

将项目分配到数组中时,它们永远不会符合垃圾回收的条件。如果您将字符串作为不是常量(实习)或null的属性,则会导致内存分配要求上升。

在第一种情况下,我怀疑发生的事情是你快速浏览对象,因此它们保留在Gen0中,并且可以快速GCed,并且内存段可以重复使用。这意味着您永远不必从操作系统分配更多内存。

在第二种情况下,您在对象中创建字符串,这两个字符串都是两个分配,然后存储这些字符串,因此它们不符合GC的条件。在某些时候,你需要获得更多的内存,所以你将获得分配的内存。

关于您的最终检查 - 当您将Name设置为null时,if (!string.IsNullOrEmpty(item.Name))检查会阻止其添加。因此,两个代码路径,以及时间,变得(有效)相同,虽然第一个稍慢(很可能是由于JIT第一次运行)。

答案 1 :(得分:2)

我的猜测是,编译器非常聪明,并且在您未分配的情况下,您认为不需要对Item执行任何重要操作。它可能只是在第一个循环中重用Item对象内存,因为它可以。在第二个循环中,需要分配堆的位,因为它们都是独立的,稍后会引用。

我猜这种与你所看到的与垃圾收集相关的内容是一致的。在第一个循环中创建了一个项目,而不是许多项目。

快速说明 - 第一个循环可能正在使用对象池,因为它是优化。 This article可能会提供见解。正如Reed很快指出的那样,文章讨论了应用优化,但我想分配器本身有很多做类似事情的优化。

答案 2 :(得分:1)

我不相信这与数组赋值有关(真的)。它与物品及其包含的物体必须保持在一起的时间量有关,以防您以后可以参考它们。这与堆分配和垃圾收集生成有关。

首次分配item时,它的字符串将在“0代”中。这通常是垃圾收集,非常热,甚至可能是高速缓存,内存。很可能在循环的下几次迭代中,整个“第0代”将被GC并且内存重新用于新的items及其字符串。当我们将赋值添加到数组时,该对象不能被垃圾收集,因为它仍然存在对它的引用。这会导致内存消耗增加。

我相信你会在代码执行过程中看到内存增加:我认为问题是堆中的内存分配加上缓存未命中,因为它总是不得不使用“新鲜”内存并且不能从硬件中受益记忆缓存。

答案 3 :(得分:0)

尝试解决您的实际问题(虽然这是一个有趣的解决难题)。我会推荐一些东西:

  1. 不要在构造中存储连接的字符串。使用get访问器返回字符串值。在诊断数组分配时,这会将字符串连接从图片中删除。如果您想在第一次get时“缓存”计算出的值,那就应该没问题。
  2. 针对您的实际计划运行dotTrace,以更好地了解花费的时间。由于没有任何东西可以来加速数组分配,所以你可以找到你可以更改的其他区域。

答案 4 :(得分:0)

在我看来,你是分支预测的受害者。让我们详细了解你在做什么:

在“没有分配”的情况下,您只需将 null 分配给数组的所有元素;通过这样做,处理器在for循环的一些迭代之后学习,即为数组项分配相同的值(甚至 null );因此不再需要 if 语句:您的程序运行得更快。

在“With assignment”的情况下,处理器不知道新生成的项目的进展: for 循环的每次迭代都会调用 if 语句;这会导致程序运行得更慢...

这种行为依赖于称为分支预测单元的处理器硬件的一部分(消耗了芯片中相当大比例的晶体管......)这里也很好地说明了类似的主题Why is it faster to process a sorted array than an unsorted array?

答案 5 :(得分:-3)

好的,我还在寻找,但是MSDN建议您使用一个集合(大概是List<T>HashTable<T>或类似的东西)而不是数组。 From the MSDN documentation

  

类库设计者可能需要做出关于何时使用数组以及何时返回集合的困难决策。虽然这些类型具有相似的使用模型,但它们具有不同的性能特征通常,您应该在添加,删除或支持操作集合的其他方法时使用集合

也许.NET规范文档中有一些内容。