考虑以下简单程序:
using System;
using System.Diagnostics;
class Program
{
private static void Main(string[] args)
{
const int size = 10000000;
var array = new string[size];
var str = new string('a', 100);
var sw = Stopwatch.StartNew();
for (int i = 0; i < size; i++)
{
var str2 = new string('a', 100);
//array[i] = str2; // This is slow
array[i] = str; // This is fast
}
sw.Stop();
Console.WriteLine("Took " + sw.ElapsedMilliseconds + "ms.");
}
}
如果我运行它,它的速度相对较快。如果我取消注释&#34;慢&#34;排队并评论&#34;快速&#34;线,它慢了5倍。请注意,在这两种情况下,它都会初始化字符串&#34; str2&#34;在循环内。在任何一种情况下都没有优化(这可以通过查看IL或反汇编来验证)。
在任何一种情况下,代码似乎都在做同样数量的工作。它需要分配/初始化一个字符串,然后为数组位置分配一个引用。唯一的区别是该引用是否是本地var&#34; str&#34;或&#34; str2&#34;。
为什么它会产生如此大的性能差异,将参考分配给&#34; str&#34; vs.&#34; str2&#34;?
如果我们看一下反汇编,就会有所不同:
(fast)
var str2 = new string('a', 100);
0000008e mov r8d,64h
00000094 mov dx,61h
00000098 xor ecx,ecx
0000009a call 000000005E393928
0000009f mov qword ptr [rsp+58h],rax
000000a4 nop
(slow)
var str2 = new string('a', 100);
00000085 mov r8d,64h
0000008b mov dx,61h
0000008f xor ecx,ecx
00000091 call 000000005E383838
00000096 mov qword ptr [rsp+58h],rax
0000009b mov rax,qword ptr [rsp+58h]
000000a0 mov qword ptr [rsp+38h],rax
&#34;慢&#34;版本还有两个&#34; mov&#34;操作所在的&#34;快速&#34;版本只有一个&#34; nop&#34;。
任何人都可以解释这里发生的事情吗?很难看出两个额外的mov操作如何导致> 5x减速,特别是因为我预计大部分时间应该花在字符串初始化上。感谢您的任何见解。
答案 0 :(得分:76)
你认为代码在任何一种情况下的工作量都相同。
但垃圾收集器在这两种情况下最终会做出截然不同的事情。
在str
版本中,在给定时间最多有两个字符串实例处于活动状态。这意味着(几乎)第0代中的所有新对象都死掉了,没有什么需要升级到第1代。由于第1代根本没有增长,因此GC没有理由尝试昂贵的“完整集合”。
在str2
版本中,所有新的字符串实例都处于活动状态。对象被提升为更高代(可能涉及将它们移动到内存中)。此外,由于现在更高的世代正在增长,GC偶尔会尝试运行完整的集合。
请注意,.NET GC倾向于花费时间与活动对象的数量呈线性关系:活动对象需要遍历并移开,而死对象根本不需要任何费用(它们只是被覆盖了下次分配内存)。
这意味着str
是垃圾收集器性能的最佳案例;虽然str2
是最糟糕的情况。
请查看您的计划的GC performance counters,我怀疑您会在程序之间看到非常不同的结果。
答案 1 :(得分:1)
不,本地参考不会很慢。
什么是缓慢的,是创建大量新的字符串实例,它们是类。快速版本重用相同的实例。这也可以被优化掉,而构造函数调用则不能。