使用引用创建类时的不同性能

本文关键字:性能 引用 创建 | 更新日期: 2023-09-27 17:49:42

我在c#中发现了一点奇怪的事情。我有一个类A,它只包含一个对A的引用,然后我在for循环的每次迭代中创建一个新的对象A,引用在前一次迭代中创建的对象。但是如果我将引用更改为在for循环之前创建的对象,就会快得多。为什么会这样呢?

class A
{
    private A next;
    public A(A next)
    {
        this.next = next;
    }
}
var a = new A(null);
for (int i = 0; i < 10*1000*1000; i++)
    a = new A(a);
// Takes 1.5s
var b = new A(null);
for (int i = 0; i < 10*1000*1000; i++)
    a = new A(b);
// Takes only 0.17s

我发现这是在实现不可变堆栈在通常的方式和通过VList,这是由于更快。

使用引用创建类时的不同性能

这段代码(你的第二个代码片段):

var b = new A(null);
for (int i = 0; i < 10*1000*1000; i++)
    a = new A(b);

在功能上等同于以下代码:

 var b = new A(null);
 a = new A(b);

你的第一个片段不一样:

var a = new A(null);
for (int i = 0; i < 10*1000*1000; i++)
    a = new A(a);

虽然看起来你扔掉了所有的东西,但最后一个参考你,不是。A的最后一个实例引用了前一个,它引用了之前的一个,它引用了之前的一个…一直回溯到1000万个对象。难怪它变慢了。

所以比较两段实际上没有达到相同结果的代码是没有意义的。使用实际有效的代码片段(第一个代码片段),而不是第二个代码片段。运行较慢的代码肯定比运行较快的代码好。

最后,c#有一个完美的集合类选择(如List<T>),工作得很好。我不知道你为什么要在这里重新发明轮子。

您正在度量使用内存的成本。慢版本分配120兆字节,所有对象都通过下一个对象引用引用。除去分配地址空间的成本,我看到了21个第0代收集,17个第1代收集和2个昂贵的第2代收集。后台GC无法提供帮助,因为分配率很高。

有bug的快速版本给了垃圾收集器一个非常轻松的时间。分配的对象不会在任何地方被引用,因此快速第0代收集收集所有对象。它使用的内存很少,只有gen #0段,没有gen #1和gen #2收集。

你可能已经发现了不可变对象的一个基本真理。类型保证很好,但这个概念不能很好地转化为机器的工作方式。处理器经过大量优化,使可变对象非常高效,而不可变对象对缓存不友好,占用大量内存,并给GC带来大量工作。最简单的例子是String vs StringBuilder,大多数程序员都知道何时切换到可变版本。我想,Roslyn这么晚才发布的一个基本原因是,要达到旧c#编译器设定的性能目标一定是一场巨大的战斗。

下运行应用程序时。. NET框架,内存在Heap上分配,使用托管代码特性,这些特性在高级编程语言(如 c#/VB)上可用。. NetJava.

当您使用关键字new创建一个类的实例时,编程语言告诉编译器它想在heap上分配内存(动态分配)。这个分配需要时间,因为它必须通过OS请求限制和进程。当您通过高级编程语言请求内存分配时,它将在底层分配一个更大的块(缓冲区)。因此,未来的"实例化"需要更少的时间,因为heap上已经有应用程序可用的内存。

David和Matt是正确的。如果您仔细分析应用程序,您会发现第一个示例

GC Generation GC Count
     0        3 
     1        17 
     2        2 

和第二个样本

GC Generation  GC Count
   0           28 
   1           0 
   2           0 

在你的第一个代码中,你创建了一个大的链表,其中仍然包含所有的对象,这些对象在应用程序终止之前都不会被释放。

• Max GC Heap Size: 119,546 MB

而第二个样本是

• Max GC Heap Size: 4,217 MB

var a = new Container();
loop
{
 a = new Container(a) 
}

将保留数据,因为a将包含对"旧"a的引用。而

b = new Container();
loop
{
   a = new Container(b) 
}

将只分配一个容器,该容器包含一个进一步的实例,但不包含先前分配对象的完整历史。这个故事的寓意是仔细看看你把你的物品放在哪里。如果你创建了一个大的节点链表,这就是你得到的结果。

这看起来像是CLR优化,因为在第二种情况下变量a是未使用的

在第一种情况下,您正在创建一个由10,000,000个连接对象组成的链,而在第二种情况下,您正在创建10,000,000个单独的对象连接到a的单个实例。我的猜测是,减速是由于框架在分配10,000,000个连接对象时必须执行堆管理,而不是随机分配单个对象。