Почему локальная ссылка var вызывает большую деградацию производительности?
Рассмотрим следующую простую программу:
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.");
}
}
Если я запустил это, он будет относительно быстрым. Если я раскомментирую "медленную" линию и закомментирую "быструю" линию, она будет более чем на 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
В "медленной" версии есть две дополнительные операции "mov", где "быстрая" версия имеет только "nop".
Может ли кто-нибудь объяснить, что здесь происходит? Трудно понять, как две дополнительные операции mov могут привести к замедлению > 5x, тем более, что я ожидаю, что большая часть времени должна быть потрачена на инициализацию строки. Спасибо за любые идеи.
Ответы
Ответ 1
Вы правы, что код делает примерно то же количество работы в любом случае.
Но сборщик мусора в обоих случаях делает очень разные вещи.
В версии str
не более двух экземпляров строки являются живыми в данный момент времени. Это означает, что почти все новые объекты в поколении 0 умирают, ничто не нужно продвигать в поколение 1. Поскольку поколение 1 вообще не растет, GC не имеет оснований для попыток дорогих "полных коллекций".
В версии str2
все новые экземпляры строк живы. Объекты получают более высокие поколения (что может включать перемещение их в памяти). Кроме того, поскольку более высокие поколения в настоящее время растут, GC иногда пытается запустить полные коллекции.
Обратите внимание, что .NET GC имеет тенденцию к тому, что время жизни линейно зависит от количества живых объектов: живые объекты должны быть перемещены и перемещены в сторону, в то время как мертвые объекты вообще ничего не стоят (они просто перезаписываются в следующий раз выделяется память).
Это означает, что str
является наилучшим вариантом для работы сборщика мусора; а str2
- наихудший вариант.
Взгляните на счетчики производительности GC для вашей программы, я подозреваю, что вы увидите очень разные результаты между программами.
Ответ 2
Нет, локальная ссылка не замедляется.
Что происходит медленно, создается множество новых экземпляров строк, которые являются классами. Хотя быстрая версия использует один и тот же экземпляр. Это также можно оптимизировать, в то время как вызов конструктора не может быть.