1

我写了一个小项目来测试类和结构的初始化之间的时间差,并将它们添加到列表中。
它只是在 foreach 循环中创建 10000000 个类,将它们添加到列表中并将所需时间写入控制台。然后对于结构也是如此。这是一个while(true)循环。每个循环的开头都以.Clear()两个列表中的 a 开头。

我的课

internal static void Main(string[] args)
    {
        var classes = new List<CoordClass>();
        var structs = new List<CoordStruct>();

        var sw = new Stopwatch();

        while (true)
        {
            classes.Clear();
            structs.Clear();
            sw.Reset();
            sw.Start();

            for (var i = 0; i < 10000000; i++)
            {
                classes.Add(new CoordClass(23, 24));
            }

            sw.Stop();
            Console.WriteLine("Classes: {0} ms ({1})", sw.ElapsedMilliseconds, classes.Count);
            sw.Reset();

            sw.Start();

            for (var i = 0; i < 10000000; i++)
            {
                structs.Add(new CoordStruct(23, 24));
            }

            sw.Stop();
            Console.WriteLine("Structs: {0} ms ({1})", sw.ElapsedMilliseconds, structs.Count);
            Console.WriteLine("===================");
        }

结构/类

 public struct CoordStruct
 {
    public int x, y;

    public CoordStruct(int p1, int p2)
    {
        x = p1;
        y = p2;
    }
 }

public class CoordClass
{
    public int x, y;

    public CoordClass(int p1, int p2)
    {
        x = p1;
        y = p2;
    }
}

我的输出如下:

900 毫秒(类)
300 毫秒(结构)
900 毫秒(类)
100 毫秒(结构)

在第一个循环之后,类添加到它的列表中并不是更快,但结构的添加要快得多。为什么??

我使用Visual Studio 2012中的附加调试器在Release版本中运行此测试。

4

4 回答 4

10

在第一个循环之后,将类添加到它的列表中并不是更快,但结构的添加要快得多。

错误的。具有类的版本的列表插入时间下降了,但您无法判断,因为它被实例创建的成本所淹没,这并不是更快。尝试在循环外创建一个实例,然后多次添加它。

然后你会看到这两者List<SomeClass>List<SomeStruct>从预分配中受益。

于 2013-08-26T18:37:20.287 回答
7

添加到预先分配的列表(这是Clear给你的)比随着项目的添加而增加列表要快得多。有关演示这一点的代码,请参见下面的第二个示例。

我不知道你的Coord类和结构是什么样的,所以我拼凑了一些。我还修改了您的程序,以将创建结构/类所花费的时间与将其添加到列表所花费的时间分开。这是输出。第一个数字是运行测试所花费的总时间。第二个数字是创建类和结构所花费的时间(不包括将它们添加到列表中所花费的时间):

Classes: 1404 ms (922.253)
Structs: 803 ms (215.9278)
===================
Classes: 1231 ms (895.7751)
Structs: 520 ms (215.7464)
===================
Classes: 1251 ms (911.6303)
Structs: 523 ms (220.119)
===================
Classes: 1337 ms (990.2042)
Structs: 519 ms (215.3085)
===================
Classes: 1251 ms (909.4082)
Structs: 521 ms (215.2579)
===================
Classes: 1237 ms (894.4974)
Structs: 522 ms (216.5798)
===================
Classes: 1289 ms (947.2457)
Structs: 525 ms (217.9129)
===================
Classes: 1226 ms (887.7574)
Structs: 520 ms (214.7768)
===================

此测试在 .NET 4.5、64 位、发布模式下运行,调试器已分离。

当然,由于 JIT 时间,第一次迭代是异常的。以第 3 次迭代为例,它非常具有代表性。课程花费了 1,251 毫秒,其中 911 毫秒是创建时间。剩下 340 毫秒用于添加和开销。

结构体耗时 523 毫秒,其中 215 毫秒是创建时间。剩下 308 毫秒用于添加和开销。称之为洗头。

您所看到的是创建类(必须在堆上分配并将对它的引用复制到列表)与在堆栈上创建结构并将该非常小的结构复制到列表的内部数组中的区别。

我的测试没有说明第一次和第二次迭代之间的差异有多少是 JIT 时间,有多少是列表重新分配。您必须对添加进行计时(就像我对创建所做的那样)才能看到差异。

不过请理解,我们说的是 1000 万次迭代的 700 毫秒差异。您必须创建很多这样的东西才能在任何非平凡程序的运行时产生任何真正的差异。

代码如下。

    private struct CoordStruct
    {
        public readonly int X;
        public readonly int Y;

        public CoordStruct(int x, int y)
        {
            X = x;
            Y = y;
        }
    }

    private class CoordClass
    {
        public readonly int X;
        public readonly int Y;

        public CoordClass(int x, int y)
        {
            X = x;
            Y = y;
        }
    }

    private void DoStuff()
    {
        const int Iterations = 10000000;
        var classes = new List<CoordClass>();
        var structs = new List<CoordStruct>();
        var sw = new Stopwatch();
        while (true)
        {
            TimeSpan createTimeStruct = TimeSpan.Zero;
            TimeSpan createTimeClass = TimeSpan.Zero;

            classes.Clear();
            structs.Clear();
            // force garbage collection so that it doesn't happen
            // in the middle of things.
            GC.Collect();
            GC.WaitForPendingFinalizers();
            sw.Reset();
            sw.Start();
            for (var i = 0; i < Iterations; i++)
            {
                var start = sw.Elapsed;
                var c = new CoordClass(23, 24);
                var stop = sw.Elapsed;
                createTimeClass += (stop - start);
                classes.Add(c);
            }
            sw.Stop();
            Console.WriteLine("Classes: {0} ms ({1})", sw.ElapsedMilliseconds, createTimeClass.TotalMilliseconds);
            sw.Reset();
            sw.Start();
            for (var i = 0; i < Iterations; i++)
            {
                var start = sw.Elapsed;
                var c = new CoordStruct(23, 24);
                var stop = sw.Elapsed;
                createTimeStruct += (stop - start);
                structs.Add(c);
            }
            sw.Stop();
            Console.WriteLine("Structs: {0} ms ({1})", sw.ElapsedMilliseconds, createTimeStruct.TotalMilliseconds);
            Console.WriteLine("===================");
        }
    }

现在,如果您想查看添加到空列表和添加到预分配列表之间的区别,请运行以下代码:

    private void DoStuff()
    {
        const int Iterations = 10000000;
        while (true)
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();

            var sw = Stopwatch.StartNew();
            var structs = new List<CoordStruct>();
            AddItems(structs, Iterations);
            sw.Stop();
            Console.WriteLine("Empty list: {0:N0} ms", sw.ElapsedMilliseconds);

            sw.Restart();
            structs = new List<CoordStruct>(Iterations);
            AddItems(structs, Iterations);
            sw.Stop();
            Console.WriteLine("Pre-allocated list: {0:N0} ms", sw.ElapsedMilliseconds);
            Console.WriteLine("===================");
        }
    }

    private void AddItems(List<CoordStruct> list, int nItems)
    {
        for (var i = 0; i < nItems; ++i)
        {
            list.Add(new CoordStruct(23, 24));
        }
    }

在我的机器上,空列表大约需要 140 毫秒,而预分配列表大约需要 100 毫秒。

于 2013-08-26T18:51:25.147 回答
4

列表没有Reset方法,但我认为您指的是Clear方法。A 在List<T>内部使用数组管理项目,随着新项目添加到列表中,该数组逐渐扩展。这只会在内部数组已满时发生,此时List将分配一个新数组并将前一个数组中的每个项目复制到新数组中 - 这是一个耗时的过程。可以通过Capacity属性检查内部数组的大小。该Clear方法将“擦除”对添加到数组中的对象的任何内部引用并将 设置Count为 0,但它使内部数组保持相同的大小。这可以用这个简单的脚本来验证:

var list = new List<int>();
for(int i = 0; i < 10000; i++)
{
    list.Add(i);
}
Console.WriteLine(list.Capacity); // 16384
list.Clear();
Console.WriteLine(list.Capacity); // 16384

所以第二次通过循环它明显更快,因为它可以避免调整内部数组的大小。这种效果在大型结构列表上更为显着,因为它们占用的内存比对类的引用更大。

为了解决为什么仅在结构上观察到这种情况的问题,这似乎是因为您没有执行适当的“热身”。当我按原样运行您的代码时,我的结果是:

Classes: 1787 ms (10000000)
Structs: 554 ms (10000000)
===================
Classes: 1618 ms (10000000)
Structs: 229 ms (10000000)
===================

在第一次和第二次迭代之间,这仅提高了 9% 的性能。但是,如果我在 while 循环之前添加一小段代码:

classes.Add(new CoordClass(23, 24));
structs.Add(new CoordStruct(23, 24));

我的结果是:

Classes: 1786 ms (10000000)
Structs: 548 ms (10000000)
===================
Classes: 1527 ms (10000000)
Structs: 216 ms (10000000)
===================

这是 14% 的性能提升。它仍然不如在结构上观察到的那么重要,但我认为这有助于解释你所看到的。

于 2013-08-26T18:17:51.590 回答
2

List<T>根本不是一个列表(在计算机科学/数据结构意义上):它是一个长度可调的数组,它使用一个固定长度的数组作为其后备存储。

您的声明List<T>没有指定初始大小,我相信初始大小是 16 个元素(尽管我已经有一段时间没有在 Reflector 中翻找了)。

当您向列表中添加元素时,每次超过后备存储数组的大小时,都会分配一个新数组,其中包含下一个大小增量,并将现有数组复制到其中。您的第一次迭代很昂贵,因为List<T>随着列表的增长必须反复重新分配和复制集合。随后的执行速度要快得多,因为它们不必做额外的工作。

FWIW,List<T>后备存储扩展的增量是非线性的(我认为大小加倍,但我不确定。)

于 2013-08-26T18:48:21.163 回答