Подтвердить что ты не робот

Почему локальная ссылка 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, тем более, что я ожидаю, что большая часть времени должна быть потрачена на инициализацию строки. Спасибо за любые идеи.

4b9b3361

Ответ 1

Вы правы, что код делает примерно то же количество работы в любом случае.

Но сборщик мусора в обоих случаях делает очень разные вещи.

В версии str не более двух экземпляров строки являются живыми в данный момент времени. Это означает, что почти все новые объекты в поколении 0 умирают, ничто не нужно продвигать в поколение 1. Поскольку поколение 1 вообще не растет, GC не имеет оснований для попыток дорогих "полных коллекций".

В версии str2 все новые экземпляры строк живы. Объекты получают более высокие поколения (что может включать перемещение их в памяти). Кроме того, поскольку более высокие поколения в настоящее время растут, GC иногда пытается запустить полные коллекции.

Обратите внимание, что .NET GC имеет тенденцию к тому, что время жизни линейно зависит от количества живых объектов: живые объекты должны быть перемещены и перемещены в сторону, в то время как мертвые объекты вообще ничего не стоят (они просто перезаписываются в следующий раз выделяется память).

Это означает, что str является наилучшим вариантом для работы сборщика мусора; а str2 - наихудший вариант.

Взгляните на счетчики производительности GC для вашей программы, я подозреваю, что вы увидите очень разные результаты между программами.

Ответ 2

Нет, локальная ссылка не замедляется.

Что происходит медленно, создается множество новых экземпляров строк, которые являются классами. Хотя быстрая версия использует один и тот же экземпляр. Это также можно оптимизировать, в то время как вызов конструктора не может быть.