Интересное поведение С#

Действительно любопытно для нижеприведенной программы (да запустите в режиме деблокирования без прикрепленного отладчика), первый цикл назначает новый объект каждому элементу массива и занимает около секунды для запуска.

Поэтому мне было интересно, какая часть занимает больше всего времени - создание или назначение объекта. Поэтому я создал второй цикл, чтобы проверить время, необходимое для создания объектов, и третий цикл для проверки времени назначения, и оба выполняются всего за несколько миллисекунд. Что происходит?

static class Program
{
    const int Count = 10000000;

    static void Main()
    {
        var objects = new object[Count];
        var sw = new Stopwatch();
        sw.Restart();
        for (var i = 0; i < Count; i++)
        {
            objects[i] = new object();
        }
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds); // ~800 ms
        sw.Restart();
        object o = null;
        for (var i = 0; i < Count; i++)
        {
            o = new object();
        }
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds); // ~ 40 ms
        sw.Restart();
        for (var i = 0; i < Count; i++)
        {
            objects[i] = o;
        }
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds); // ~ 50 ms
    }
}

Ответы

Ответ 1

Когда объект, занимающий менее 85 000 байт ОЗУ и не являющийся массивом double, создается, он помещается в область памяти, называемую кучей Generation Zero. Каждый раз, когда куча Gen0 растет до определенного размера, каждый объект в куче Gen0, к которому система может найти живую ссылку, копируется в кучу Gen1; куча Gen0 затем размалывается по всей массе, поэтому у нее есть место для новых объектов. Если куча Gen1 достигает определенного размера, все, на что ссылается ссылка, будет скопировано в кучу Gen2, после чего кучу Gen0 можно удалить с помощью массива.

Если многие объекты создаются и сразу заброшены, куча Gen0 будет многократно заполняться, но очень мало объектов из кучи Gen0 нужно будет скопировать в кучу Gen1. Следовательно, куча Gen1 будет заполняться очень медленно, если вообще. В отличие от этого, если большинство объектов в куче Gen0 по-прежнему ссылаются при заполнении кучи Gen0, системе придется копировать эти объекты в кучу Gen1. Это заставит систему тратить время на копирование этих объектов, а также кучу Gen1, чтобы заполнить ее настолько, что ее придется сканировать для живых объектов, и все живые объекты оттуда должны быть скопированы снова в кучу Gen2, Все это занимает больше времени.

Другая проблема, которая замедляет работу вашего первого теста, заключается в том, что при попытке идентифицировать все живые объекты Gen0 система может игнорировать любые объекты Gen1 или Gen2, только если они не были затронуты со времени последней коллекции Gen0. Во время первого цикла массив objects будет постоянно касаться; следовательно, каждой коллекции Gen0 придется потратить время на ее обработку. Во втором цикле он не касался вообще, так что даже если будет столько же коллекций Gen0, что они не займут столько времени, сколько нужно. Во время третьего цикла массив будет постоянно касаться, но никаких новых объектов кучи не создаются, поэтому циклов сбора мусора не потребуется, и не имеет значения, сколько времени они будут выполнять.

Если бы вы добавили четвертый цикл, который создал и оставил объект на каждом проходе, но который также запомнил в слоте массива ссылку на ранее существовавший объект, я бы ожидал, что это займет больше времени, чем в комбинированное время второго и третьего циклов, хотя он будет выполнять те же операции. Возможно, не так много времени, как первый цикл, так как очень немногие из вновь созданных объектов нужно будет скопировать из кучи Gen0, но дольше, чем второй, из-за дополнительной работы, необходимой для определения того, какие объекты все еще живы. Если вы хотите исследовать вещи еще дальше, может быть интересно провести пятый тест с вложенным циклом:

for (int ii=0; ii<1024; ii++)
  for (int i=ii; i<Count; i+=1024)
     ..

Я не знаю точных данных, но .NET пытается избежать сканирования целых больших массивов, из которых только небольшая часть затрагивается путем разбивки их на куски. Если затронут кусок большого массива, все ссылки внутри этого фрагмента должны быть отсканированы, но ссылки, хранящиеся в кусках, которые не были затронуты с момента последней коллекции Gen0, могут быть проигнорированы. Разрыв цикла, как показано выше, может привести к тому, что .NET перестанет касаться большинства блоков в массиве между коллекциями Gen0, что, возможно, приведет к более медленному времени, чем первый цикл.

Ответ 2

  • Вы создаете объекты 10 миллионов и сохраняете их в разных местах в памяти. Потребление памяти здесь самое высокое.
  • Вы создаете 10 миллионов объектов, но они нигде не хранятся, просто отброшен.
  • Вы создаете 1 объект и составляете 10 миллионов ссылок на него, минимальный потребление памяти.

И да, производительность analisys ниже для 10 тысяч объектов (10 миллионов заняли бы toooo long).

Performance for ONLY 10 thousand objects

UPDATE:. На этой диаграмме показана работа ЦП для распределения памяти в первом случае. Обратите внимание, что функция [email protected]@... занимает 80,5% времени процессора.

CPU performance case 1

UPDATE2: и для полноты процессорного времени для CaseTwo.

CPU performance case 2

UPDATE3: Только для полноты, третьего случая

CPU performance case 3