2015-05-19 27 views
1

我在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 

我实现用常规方法不变堆栈的过程中发现这一点,并throught VList这是由于快得多。

+4

有人猜测,这是因为在第二种情况下,每次调用“a = new A(b)”时,对旧的“A”实例的引用都会丢失,因此CLI可以重新使用其内存。在第一种情况下,必须分配额外的内存来保存您创建的1000万个实例,因为您不断引用它们。 –

+0

小提示,但做这样的时间测试时,总是做一些基本的理性/正确性检查数据,特别是当你看到这样的天文数据不同的时间结果。你不想对快速但不正确的解决方案太过兴奋。正如他们所说,给错误结果两倍的东西无限慢。 –

回答

0

它看起来像CLR优化,因为在第二种情况下变量a未被使用。

2

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

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的最后一个实例引用了前一个实例,该实例引用了之前的实例,它引用了之前的实例...一直回溯到10,000,000个对象。难怪它更慢。

因此比较实际上没有达到相同结果的两段代码是毫无意义的。使用实际工作的那个(你的第一个片段)而不是第二个片段。运行较慢的代码是,肯定是好于没有速度较快的代码。

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

+0

这似乎是明显的答案,但我不知道OP如何衡量所花的时间。没有办法'var b = new A(null); a = new A(b);'需要170ms执行。 –

+0

@JasonWatkins:考虑到这一点,并且稍微玩一下,我认为它更像是像其他人所建议的那样重用堆内存。我不认为它实际上可以完全优化循环。 –

+0

我没有重新发明轮子,我实现了**不变的**集合,它保留所有版本(请参阅函数式编程中的数据结构)。我想这应该是某种JIT优化,但不是你提到的。在VList中,我创建了所有的值,所以JIT不能跳过for循环。 – Mayo

0

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

1

当您运行下.NET框架的应用程序,内存上Heap使用托管代码引擎盖下分配的可用像C#/ VB.Net的Java高级编程语言功能

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

1

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) 
} 

将保留数据,因为将包含对“老”一参考。 而

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

将分配onle其中包含一个进一步的实例,但以前未分配的对象的完整历史一个容器。 故事的寓意是仔细观察你的根源。如果你创建一个大的节点链表,它就是你得到的。

2

您正在测量使用内存的成本。缓慢版本分配120兆字节,所有对象通过下一个对象引用引用。从分配地址空间的代价来看,我看到了21个第#0个集合,第17个第1个集合和第2个昂贵的第2代集合。背景GC无法提供帮助,因为分配率很高。

越野车快速版给垃圾收集器一个非常容易的时间。分配的对象在任何地方都没有引用,所以快速gen#0集合收集所有这些对象。它使用非常小的内存,只有gen#0段,并且没有gen#1和gen#2集合。

否则,您可能已经发现了关于不可变对象的基本事实。类型保证是好的,但这个概念并没有很好地表达机器的工作方式。处理器进行了大量优化,使可变对象非常高效,不可变的对象缓存不友好,内存不足,给GC大量工作。最简单的例子是String vs StringBuilder,大多数程序员知道什么时候切换到可变版本。我认为,为什么罗斯林为时太晚,满足旧C#编译器所设定的性能目标一定是一场巨大的战斗,我认为其中一个基本原因。