考虑以下简单程序:
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.");
}
}
Run Code Online (Sandbox Code Playgroud)
如果我运行它,它的速度相对较快.如果我取消注释"慢"线并注释掉"快速"线,它的速度会慢5倍.请注意,在这两种情况下,它都会在循环内初始化字符串"str2".在任何一种情况下都没有优化(这可以通过查看IL或反汇编来验证).
在任何一种情况下,代码似乎都在做同样数量的工作.它需要分配/初始化一个字符串,然后为数组位置分配一个引用.唯一的区别是该引用是否是本地var"str"或"str2".
为什么它会产生如此大的性能差异,分配对"str"和"str2"的引用?
如果我们看一下反汇编,就会有区别:
(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
Run Code Online (Sandbox Code Playgroud)
"慢"版本有两个额外的"mov"操作,其中"快速"版本只有一个"nop".
谁能解释一下这里发生了什么?很难看出两个额外的mov操作如何导致> 5x减速,特别是因为我预计大量的时间应该花在字符串初始化上.感谢您的任何见解.
Dan*_*iel 76
你是对的,在任何一种情况下代码的工作量相同.
但垃圾收集器在这两种情况下最终会做出截然不同的事情.
在该str
版本中,在给定时间最多有两个字符串实例处于活动状态.这意味着(几乎)第0代中的所有新对象都死亡,没有什么需要升级到第1代.由于第1代根本没有增长,因此GC没有理由尝试昂贵的"完整集合".
在该str2
版本中,所有新的字符串实例都是活动的.对象被提升为更高代(可能涉及将它们移动到内存中).此外,由于现在更高的世代正在增长,GC偶尔会尝试运行完整的集合.
请注意,.NET GC往往需要时间与活动对象的数量成线性关系:活动对象需要遍历和移动,而死对象根本不需要任何费用(它们只是在下次内存时被覆盖分配).
这意味着str
垃圾收集器性能最佳; 虽然str2
是最糟糕的情况.
看看你的程序的GC性能计数器,我怀疑你会发现程序之间的结果差别很大.