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

Mil*_*ian 19 .net c# 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

我的结果

  • 当我Release在我的机器上运行构建(优化打开)时,我得到以下结果:

    Without assignment: 00:00:00.2193348
       With assignment: 00:00:00.8819170
    
    Run Code Online (Sandbox Code Playgroud)

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

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

    public Item(int i)
    {
        //this.Name = "Hello " + i;
        this.Name = "Hello";
        //this.Name = null;
    }
    
    Run Code Online (Sandbox Code Playgroud)

    我得到以下结果:

    Without assignment: 00:00:00.0228067
       With assignment: 00:00:00.0718317
    
    Run Code Online (Sandbox Code Playgroud)

    分配的循环仍然比没有分配的循环慢约3倍

  • 最后,如果我分配null给该Name属性:

    public Item(int i)
    {
        //this.Name = "Hello " + i;
        //this.Name = "Hello";
        this.Name = null;
    }
    
    Run Code Online (Sandbox Code Playgroud)

    我得到以下结果:

    Without assignment: 00:00:00.0146696
       With assignment: 00:00:00.0105369
    
    Run Code Online (Sandbox Code Playgroud)

    一旦没有分配字符串,没有赋值的版本最终会稍微慢一点(我假设因为所有这些实例都被释放用于垃圾回收)

问题

  • 为什么数组项目分配会使测试程序变得如此之慢?

  • 是否有一个属性/语言构造/等会加速分配?

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

Ree*_*sey 26

我怀疑大多数时序问题都与内存分配有关.

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

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

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

至于你的最后检查 - 当你设置Name为时null,if (!string.IsNullOrEmpty(item.Name))检查将阻止它被添加.因此,两个代码路径以及因此时序变得(有效地)相同,尽管第一个稍微慢一点(很可能是由于JIT第一次运行).

  • 这是对的.如果在`items [i] = item;`之后立即添加行`items [i] = null;`,那么两个循环的时间突然变得几乎相同.我认为这表明GC和内存处理导致了速度减慢. (9认同)