Должен ли программист действительно заботиться о том, сколько и/или как часто создаются объекты в .NET?

Этот вопрос озадачил меня в течение долгого времени. Я родом из тяжелого и длинного фона С++, и с тех пор, как я начал программировать на С# и занимался сборкой мусора, у меня всегда было ощущение, что такая "магия" будет стоить дорого.

Недавно я начал работать в большом проекте MMO, написанном на Java (на стороне сервера). Моя основная задача - оптимизировать потребление памяти и использование ЦП. Сотни тысяч сообщений в секунду отправляются и создается одинаковое количество объектов. После большого профилирования мы обнаружили, что сборщик мусора VM потреблял много процессорного времени (из-за постоянных коллекций) и решил попытаться свести к минимуму создание объектов, используя пулы, где это применимо, и повторно использовать все, что мы можем. До сих пор это была действительно хорошая оптимизация.

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

Итак, это также верно для .NET? если это так, в какой степени?

Я часто пишу пары таких функций:

// Combines two envelopes and the result is stored in a new envelope.
public static Envelope Combine( Envelope a, Envelope b )
{
    var envelope = new Envelope( _a.Length, 0, 1, 1 );
    Combine( _a, _b, _operation, envelope );
    return envelope;
}

// Combines two envelopes and the result is 'written' to the specified envelope
public static void Combine( Envelope a, Envelope b, Envelope result )
{
    result.Clear();
    ...
}

Вторая функция предоставляется в случае, если у кого-то есть уже сделанный конверт, который можно использовать повторно для хранения результата, но я нахожу это немного странным.

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

Я знаю, что в качестве разработчика .NET я не должен беспокоиться об этих проблемах, но мой опыт работы с Java и здравым смыслом подсказывает мне, что я должен.

Любой свет и мысли по этому вопросу будут высоко оценены. Спасибо заранее.

Ответ 1

Да, это правда и для .NET. У большинства из нас есть роскошь игнорировать детали управления памятью, но в вашем случае - или в случаях, когда высокая громкость вызывает перегрузку памяти, - тогда требуется некоторая оптимизация.

Одна оптимизация, которую вы могли бы рассмотреть в своем случае - то, о чем я думал о написании статьи, на самом деле, - это комбинация структур и ref для реального детерминированного удаления.

Поскольку вы исходите из фона на С++, вы знаете, что на С++ вы можете создать экземпляр объекта либо в куче (используя новое ключевое слово, так и возвращая указатель) или в стеке (путем создания его как примитивного типа, т.е. MyType myType;). Вы можете передавать элементы, связанные с стекем, со ссылкой на функции и методы, сообщая функции принимать ссылку (используя ключевое слово & до имени параметра в объявлении). Выполнение этого сохраняет выделенный в стеке объект в памяти до тех пор, пока метод, в котором он был выделен, остается в области видимости; как только он выходит из сферы действия, объект восстанавливается, деструктор вызывается, ba-da-bing, ba-da-boom, Bob yer Uncle, и все делается без указателей.

Я использовал этот трюк, чтобы создать некоторый удивительно совершенный код в дни С++ - за счет большего стека и риска, естественно, но тщательный анализ позволил сохранить этот риск очень минимальным.

Могу сказать, что вы можете сделать тот же трюк в С#, используя structs и refs. Компромиссы? В дополнение к риску, если вы не внимательны или используете большие объекты, вы не ограничены наследованием, и вы плотно соединяете свой код, делая его менее проверяемым и менее ремонтопригодным. Кроме того, вам все равно придется решать проблемы, когда вы используете вызовы основной библиотеки.

Тем не менее, возможно, стоит посмотреть в вашем случае.

Ответ 2

Это одна из тех проблем, когда очень сложно определить окончательный ответ таким образом, который поможет вам..NET GC очень хорошо настраивается на потребности вашего приложения в памяти. Достаточно ли достаточно, чтобы ваше приложение можно было закодировать, если вам не нужно беспокоиться об управлении памятью? Я не знаю.

Есть определенные вещи здравого смысла, которые вы можете сделать, чтобы убедиться, что вы не забиваете GC. Использование типов значений, безусловно, является одним из способов достижения этого, но вам нужно быть осторожным, чтобы вы не вводили другие проблемы с плохо написанными структурами.

По большей части, однако, я бы сказал, что GC будет хорошо справляться со всем этим для вас.

Ответ 3

Я видел слишком много случаев, когда люди "оптимизировали" дерьмо из своего кода, не заботясь о том, насколько хорошо он написан или насколько хорошо он работает. Я думаю, что первая мысль должна пойти на то, чтобы сделать код решающим деловым вопросом. Код должен быть хорошо обработан и легко ремонтирован, а также проверен надлежащим образом.

После всего этого оптимизация должна быть рассмотрена, если тестирование указывает на необходимость.

Ответ 4

Случайный совет:

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

Другой способ, который вы можете использовать в С#, - это сбрасывание на собственный С++ для ключевых частей, которые недостаточно эффективны... и затем используйте Dispose шаблон на С# или С++/CLI для управляемых объектов, которые содержат неуправляемые ресурсы.

Кроме того, убедитесь, что когда вы используете типы значений, которые вы не используете, таким образом, чтобы они неявно помещали их в поле и помещали их в кучу, что может быть легко сделать из Java.

Наконец, обязательно найдите хороший профилировщик памяти.

Ответ 5

У меня такая же мысль все время.

Правда, хотя нас учили следить за ненужными тактами процессора и потреблением памяти, стоимость небольших недостатков в нашем коде практически ничтожна на практике.

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

И вообще, как говорится, преждевременная оптимизация - это корень всего зла. Вы можете потратить часы на оптимизацию одной маленькой функции, и получается, что какая-то другая часть кода дает узкое место. Просто держите равновесие, делая это просто и делайте это глупо.

Ответ 6

Хотя сборщик мусора есть, плохой код остается плохим кодом. Поэтому я бы сказал, что да, как разработчик .Net, вам все равно нужно заботиться о том, сколько объектов вы создаете, а главное - писать оптимизированный код.

Я видел, что значительное количество проектов отклоняется из-за этой причины в обзорах кода внутри нашей среды, и я твердо верю, что это важно.

Ответ 7

Управление памятью в формате .NET очень хорошее и возможность программно настроить GC, если вам нужно, это хорошо.

Мне нравится, что вы можете создавать свои собственные методы Dispose на классах, наследуя их от IDisposable и настраивая их на ваши нужды. Это отлично подходит для обеспечения того, чтобы соединения с сетями/файлами/базами данных всегда очищались и не просачивались таким образом. Существует также беспокойство по поводу очистки слишком рано.

Ответ 8

Возможно, вы захотите написать набор кэшей объектов. Вместо создания новых экземпляров вы можете сохранить список доступных объектов где-нибудь. Это поможет вам избежать GC.

Ответ 9

Я согласен со всеми указанными выше пунктами: сборщик мусора отлично, но его нельзя использовать в качестве костыля.

Я просидел много времени в обзорах кода, обсуждая более тонкие точки CLR. Лучший окончательный ответ - разработать культуру производительности в вашей организации и активно профилировать ваше приложение с помощью инструмента. Будут обозначены узкие места, и вы будете обращаться по мере необходимости.

Ответ 10

Я думаю, вы ответили на свой вопрос - если это станет проблемой, тогда да! Я не думаю, что это вопрос .Net против Java, это действительно вопрос "должны ли мы идти на исключительную длину, чтобы избежать выполнения определенных видов работы". Если вам нужна более высокая производительность, чем у вас, и после выполнения какого-либо профилирования вы обнаружите, что для создания объекта или сбора мусора требуется много времени, тогда пришло время попробовать какой-то необычный подход (например, упомянутый вами пул).

Ответ 11

Мне жаль, что я не был "легендой программного обеспечения" и мог говорить об этом своим голосом и дыханием, но, поскольку я не уверен, я полагаюсь на SL для таких вещей.

Я предлагаю следующее сообщение в блоге Andrew Hunter по .NET GC было бы полезно:

http://www.simple-talk.com/dotnet/.net-framework/understanding-garbage-collection-in-.net/

Ответ 12

Даже за пределами аспектов производительности семантика метода, который изменяет передаваемый объект mutable, часто будет более чистым, чем семантика метода, который возвращает новый изменяемый объект на основе старого. Заявления:

munger.Munge(someThing, otherParams);
someThing = munger.ComputeMungedVersion(someThing, otherParams);

может в некоторых случаях вести себя одинаково, но в то время как первое делает одно, последнее будет делать два эквивалента:

someThing = someThing.Clone(); // Or duplicate it via some other means
munger.Munge(someThing, otherParams);

Если someThing является единственной ссылкой, в любом месте юниверса, конкретному объекту, а затем заменить его ссылкой на клон будет no-op, и поэтому изменение переданного объекта будет эквивалентно возвращая новый. Если, однако, someThing идентифицирует объект, к которому существуют другие ссылки, прежний оператор будет изменять объект, идентифицированный всеми этими ссылками, оставив все ссылки, прикрепленные к нему, в то время как последний приведет к тому, что someThing станет "отделенным",.

В зависимости от типа someThing и того, как он используется, его вложение или отсоединение могут быть спорными проблемами. Вложение было бы актуальным, если какой-либо объект, который содержит ссылку на объект, мог бы изменить его, пока существуют другие ссылки. Прикрепление является спорным, если объект никогда не будет изменен или если не существует ссылок вне someThing. Если можно показать, что какое-либо из последних условий применимо, то замена someThing ссылкой на новый объект будет прекрасной. Однако, если тип someThing не является неизменным, для такой демонстрации потребуется документация за пределами объявления someThing, поскольку .NET не предоставляет стандартных средств аннотации того, что конкретная ссылка будет идентифицировать объект, который, несмотря на то, что он изменяемый тип - никто не может изменять, или аннотировать, что конкретная ссылка должна быть единственной в юниверсе, которая идентифицирует конкретный объект.