2017-01-21 3 views
2

Какой из следующих двух частей кода будет лучше работать в разных случаях и почему?GetOrAdd new vs factory performance

.

private readonly ConcurrentDictionary<int, List<T>> _coll; 
_coll.GetOrAdd(1, new List<T>()); 

Это создает новый List при каждом вызове, даже если он не нужен (Сколько это утверждение еще вопрос, если мы передаем capacity в 0?).

.

private readonly ConcurrentDictionary<int, List<T>> _coll; 
_coll.GetOrAdd(1, (val) => new List<T>()); 

Это только создает List по требованию, но есть делегат вызова.

ответ

4

С точки зрения памяти первый способ будет вызывать распределение каждый раз, а второй будет использовать объект кешированного делегата, поскольку он не фиксирует никаких переменных. Компилятор обрабатывает генерацию кэшированного делегата. Нет никакой разницы в первом случае для емкости, равной нулю, поскольку конструктор по умолчанию для List<T> использует пустой инициализационный массив, то же, что и явная емкость 0.

Что касается инструкций по исполнению, то они являются такими же, когда ключ найден, поскольку второй аргумент не используется. Если ключ не найден, первый способ просто должен прочитать локальную переменную, а второй способ будет иметь слой косвенности для вызова делегата. Кроме того, looking into the source code, похоже, что GetOrAdd с фабрикой сделает дополнительный поиск (через TryGetValue), чтобы избежать вызова фабрики. Делегат также может быть выполнен несколько раз. GetOrAdd просто гарантирует, что вы видите одну запись в словаре, а не то, что фабрика вызывается только один раз.

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

Также любая реализация, использующая это, скорее всего, должна реализовать блокировку вокруг List<T>, которая возвращается, поскольку она не является потокобезопасной.

+0

В качестве примера я использовал «Список ». Фактическая коллекция на своем месте является потокобезопасной. Кроме того, принимая предложение Иосифа, делает '() => Enumerable.Empty ()' помощь с производительностью? – Hele

+1

Кроме того, он зависит от того, какая версия рамки используется (и было ли реализовано новое поведение), как это было в .NET 4.5, где изменения имели некоторые нежелательные побочные эффекты. Обратитесь к https://basildoncoder.com/blog/concurrentdictionary-getoradd-vs.html – Alex

+0

@Hele Ну, если вы собираетесь использовать 'Enumerable.Empty ()', то делегат не нужен.'Enumerable.Empty ()' просто читает статическое поле readonly. Однако я не уверен, почему вы используете интерфейс только для чтения, например 'IEnumerable '. Я подозреваю, что это сделает обновления сложнее, но мне нужно будет увидеть больше кода. –

0

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

Мое предложение было бы использовать Enumerable.Empty<T>(), поскольку вы будете сохранять себе выделение для каждого элемента массива.