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

Этот вопрос уже давно озадачивает меня. Я имею богатый и длинный опыт работы с C ++, и с тех пор, как я начал программировать на C # и заниматься сборкой мусора, у меня всегда было чувство, что такая «магия» будет стоить дорого.

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

Итак, из того, что я узнал, наличие сборщика мусора - это круто, но вы не можете просто притвориться, что его не существует, и вам все равно нужно позаботиться о создании объекта и его последствиях (по крайней мере, в 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 и здравый смысл подсказывают мне, что я должен.

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

Ответы [ 12 ]

9 голосов
/ 15 июля 2009

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

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

Поскольку вы пришли из фона C ++, вы знаете, что в C ++ вы можете создавать экземпляр объекта либо в куче (используя ключевое слово new и возвращая указатель), либо в стеке (создавая его как примитивный тип, т.е. MyType myType;). Вы можете передать элементы, выделенные в стеке, по ссылке на функции и методы, указав функции принять ссылку (используя ключевое слово & перед именем параметра в вашем объявлении). При этом объект, выделенный из стека, остается в памяти до тех пор, пока метод, в котором он был выделен, остается в области видимости; как только он выходит из области видимости, объект восстанавливается, вызывается деструктор, ба-да-бинг, ба-да-бум, дядя Боба, и все это делается без указателей.

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

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

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

5 голосов
/ 15 июля 2009

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

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

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

3 голосов
/ 15 июля 2009

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

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

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

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

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

3 голосов
/ 15 июля 2009

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

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

1 голос
/ 25 января 2014

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

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

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

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

Если someThing является единственной ссылкой где-либо в юниверсе на конкретный объект, то замена его ссылкой на клон будет запрещена, и поэтому изменение переданного объекта будет эквивалентно возвращая новый. Однако, если someThing идентифицирует объект, на который существуют другие ссылки, первый оператор изменит объект, идентифицированный всеми этими ссылками, оставив все ссылки, прикрепленные к нему, в то время как последний вызовет someThing «отсоединение» .

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

1 голос
/ 15 июля 2009

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

Я предлагаю следующее сообщение Эндрю Хантера в блоге .NET GC: * ​​1003 *

http://www.simple -talk.com / DotNet / .net-основа / understanding-garbage-collection-in-.net /

1 голос
/ 15 июля 2009

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

1 голос
/ 15 июля 2009

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

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

1 голос
/ 15 июля 2009

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

1 голос
/ 15 июля 2009

.NET Управление памятью очень хорошо, и возможность программной настройки ГХ, если вам нужно, хороша.

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

...