Так же могут понадобиться не только массивы, поэтому попробуем написать универсальный пул. Заранее скажу что есть готовые решения.
Нам понадобится хранилище и методы для создания и очистки элемента в хранилище.
public sealed class Pool<T>
{
readonly List<T> _items = new List<T>();
readonly Func<T> _itemCreator = null;
readonly Action<T> _itemClearer = null;
readonly int _maxItems;
SpinLock _lock;
public Pool(Func<T> itemCreator, int maxItems)
{
_itemCreator = itemCreator
?? throw new ArgumentNullException(nameof(itemCreator));
_maxItems = maxItems;
}
public Pool(Func<T> itemCreator, Action<T> itemClearer, int maxItems)
: this(itemCreator, maxItems)
{
_itemClearer = itemClearer
?? throw new ArgumentNullException(nameof(itemClearer));
}
public Pooled Rent()
{
T rented;
bool lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
if (_items.Any())
{
rented = _items[0];
_items.RemoveAt(0);
}
else
rented = _itemCreator();
}
finally
{
if (lockTaken)
_lock.Exit();
}
return new Pooled(rented, this);
}
public int Count => _items.Count;
public class Pooled : IDisposable
{
readonly Pool<T> _pool;
public readonly T item;
public Pooled(T item, Pool<T> pool)
{
this.item = item;
_pool = pool;
}
bool disposedValue = false;
public void Dispose()
{
bool lockTaken = false;
try
{
_pool._lock.Enter(ref lockTaken);
var dValue = Volatile.Read(ref disposedValue);
if (!dValue)
{
Volatile.Write(ref disposedValue, true);
if (_pool.Count < _pool._maxItems)
{
_pool._itemClearer?.Invoke(item);
_pool._items.Add(item);
}
}
}
finally
{
if (lockTaken)
_pool._lock.Exit();
}
}
}
}
Метод Rent берет элемент обернутый в Pooled или создает его, а возврат произойдет в Dispose.
Конструктор с одним параметром означает что элементы не будут очищаться.
Приведу пример для List:
var bigListPool = new Pool<List<long>>(Creator, Clearer, 10);
using (var pooledList = bigListPool.Rent())
{
var list = pooledList.item;
}
Создание и очистка List:
List<long> Creator() => new List<long>(1024 * 1024);
void Clearer(List<long> l) => l.ForEach(i => i = 0);
Если список другого размера то надо создать еще один пул. Далее можно все это дело обернуть и внедрить синглтоном через ConfigureServices.
Проект
Комментарии (129)
lair
21.05.2019 00:27Ну то есть нам понадобился объект ненадолго, мы его создали, "вернули" в пул, и он теперь там лежит до морковкиного заговения и ест ресурсы, да?
Ну вы бы хотя бы слабые ссылки прикрутили, что ли.
(это не вдаваясь в те детали, что за исключением случаев, когда вы очень хорошо понимаете, что вы делаете, писать свой пул не надо)
Ascar Автор
21.05.2019 00:41Если дополнительно оборачивать через WeakReference, то после очередной уборки 2 го поколения пул будет состоять из пустых оберток, и придется создавать опять требуемый набор больших объектов. Нужно чтобы они не очищались для поддержания производительности.
и он теперь там лежит до морковкиного заговения и ест ресурсы, да?
Как бы подразумевается что эти объекты используются постоянно. Если у вас некий объект быт создан единожды и стал не нужен, то не стоит использовать пул. Если вы про удаление объектов при уменьшаемой нагрузке, то можно подумать, надо как то ее измерять со временем.lair
21.05.2019 00:51после очередной уборки 2 го поколения пул будет состоять из пустых оберток
Если у вас случился уборка второго поколения, то может это и к лучшему.
придется создавать опять требуемый набор больших объектов
Зато между моментом GC и следующим использованием объекта у приложения будет больше доступной памяти (и меньше ее фрагментация).
Нужно чтобы они не очищались для поддержания производительности.
… поэтому давайте забьем LOH, чтобы его нужно было чаще очищать, чтобы GC сильнее тормозил приложение.
Как бы подразумевается что эти объекты используются постоянно.
Вот это как раз ошибочное "подразумевается". Для массивов-то оно понятно, массивы — это буфера, они часто нужны, и их можно подобрать одного переиспользуемого размера. А вот какие еще объекты вам так часто надо переиспользовать, что вы для этого сделали пул, и получили гарантированный измеримый выигрыш в используемых ресурсах?
Более того, что вообще у вас за объекты такие, которые попадают в LOH?
Ascar Автор
21.05.2019 01:01Если у вас случился уборка второго поколения, то может это и к лучшему.
Будут удалены объекты пула которые еще необходимы, вы ведь не сможете определить нужны они или нет.
Зато между моментом GC и следующим использованием объекта у приложения будет больше доступной памяти (и меньше ее фрагментация).
вот и будет объект по новой создаваться и убираться уборщиком.
… поэтому давайте забьем LOH, чтобы его нужно было чаще очищать, чтобы GC сильнее тормозил приложение.
Так GC квоты сам определяет, он умный.
Вот это как раз ошибочное «подразумевается». Для массивов-то оно понятно, массивы — это буфера, они часто нужны, и их можно подобрать одного переиспользуемого размера. А вот какие еще объекты вам так часто надо переиспользовать, что вы для этого сделали пул, и получили гарантированный измеримый выигрыш в используемых ресурсах?
Людям из предыдущего поста понадобилось. Собственно поэтому я и задумался о пуле.lair
21.05.2019 01:11Будут удалены объекты пула которые еще необходимы
Что значит "необходимы"?
вот и будет объект по новой создаваться и убираться уборщиком.
Ну и прекрасно же.
Так GC квоты сам определяет, он умный.
Вот именно. Он умный, но вы считаете, что умнее его, и создаете пул, который мешает ему делать свою работу. Не надо так.
Людям из предыдущего поста понадобилось.
Им как раз не понадобилось, они решили, что переиспользование им не выгодно. Им понадобилось уменьшить нагрузку на LOH, и они это успешно сделали.
А какой сценарий у вас, что вам нужен пул объектов (не массивов), и вы имеете от него измеримый выигрыш?
Ascar Автор
21.05.2019 01:32Что значит «необходимы»?
Значит этот объект в скором времени будет запрошен.
Ну и прекрасно же.
Возвращаемся к проблеме которая и была. Это не прекрасно…
Вот именно. Он умный, но вы считаете, что умнее его, и создаете пул, который мешает ему делать свою работу. Не надо так.
Как раз не считаю, и создаю долгоживущие большие объекты, все по логике GC.
Им как раз не понадобилось, они решили, что переиспользование им не выгодно. Им понадобилось уменьшить нагрузку на LOH, и они это успешно сделали.
А какой сценарий у вас, что вам нужен пул объектов (не массивов), и вы имеете от него измеримый выигрыш?
Там большой объект просто разбит на маленькие. Если бы GC считал что это норма, то и клал бы все в 0е поколение. То решение работает, но мне по аналогии ArrayPool кажется правильнее делать через пул.
А какой сценарий у вас, что вам нужен пул объектов (не массивов), и вы имеете от него измеримый выигрыш?
Например List. У нас же не одни только массивы существуют.lair
21.05.2019 01:37Значит этот объект в скором времени будет запрошен.
Вы не можете знать, будет ли он в скором времени запрошен.
Возвращаемся к проблеме которая и была.
А была ли?
Там большой объект просто разбит на маленькие.
И это успешно решило проблему.
То решение работает, но мне по аналогии ArrayPool кажется правильнее делать через пул.
Вам кажется. У вас нет работающего решения, которое было бы измерено под нагрузкой.
Например List. У нас же не одни только массивы существуют.
ОтветитьList
— это не сценарий. Я спрашиваю про конкретный сценарий использования, где у вас есть просадка в производительности, которую вы успешно решили за счет использования вашего пула объектов (в данном случае — списков).Ascar Автор
21.05.2019 01:57Вы не можете знать, будет ли он в скором времени запрошен.
Если создается пул значит эти объекты должны будут часто использоваться иначе его не надо создавать.
А была ли?
Да, большой логически коротко живущий объект.
И это успешно решило проблему.
Я описываю свой подход в этой статье и ни с кем не спорю по реализациям той статьи.
Вам кажется. У вас нет работающего решения, которое было бы измерено под нагрузкой.
Решения работающего с этим классом нет, поэтому то я и делюсь идеями.
List — это не сценарий. Я спрашиваю про конкретный сценарий использования, где у вас есть просадка в производительности, которую вы успешно решили за счет использования вашего пула объектов (в данном случае — списков).
В этой статье я и не опираюсь на происходящее до этого реальное использование.lair
21.05.2019 12:09Если создается пул значит эти объекты должны будут часто использоваться иначе его не надо создавать.
"Должны" — не эквивалентно "будут". Что хуже, вы не знаете, сколько объектов вам надо, но при этом создаете их неограниченное количество.
Да, большой логически коротко живущий объект.
Это не проблема, это что-то придуманное. Проблема — это когда вы нашли и измерили. А вы, по вашим же заверениям, этого не сделали.
Я описываю свой подход в этой статье
… при этом ссылаясь на задачу из той статьи, хотя ваш подход ее не решает. Поэтому давайте просто вообще забудем, что та статья была, ваш подход к ней не подходит.
Решения работающего с этим классом нет, поэтому то я и делюсь идеями.
В этой статье я и не опираюсь на происходящее до этого реальное использование.Вот не надо так делать с ресурсными оптимизациями, вы не знаете ни что вам в реальности будет нужно, ни какие там проблемы, ни какие ограничения. Сначала нагрузка и измерения, потом решение.
Ascar Автор
21.05.2019 12:32«Должны» — не эквивалентно «будут». Что хуже, вы не знаете, сколько объектов вам надо, но при этом создаете их неограниченное количество.
Их число соответствует числу процессоров по понятной причине. Вы пример из исходников не смотрели.
Это не проблема, это что-то придуманное.
Эта проблема описана не мной, но я предлагаю альтернативное решение.
хотя ваш подход ее не решает.
Ее вполне может решить. Я же не просто так обратил внимание на ArrayPool. До этого провел проверку, большие объекты без него создаются у удаляются, с ним только 4, на 4х ядерном процессоре.
вы не знаете ни что вам в реальности будет нужно, ни какие там проблемы, ни какие ограничения
Почему не знаю? Вполне себе представляю. Синтетический пример на net core mvc после прочтения той статьи запускал и проблема проглядывалась.lair
21.05.2019 12:52Их число соответствует числу ядер процессора по понятной причине.
Ээээ, какой такой "понятной причине"?
Эта проблема описана не мной, но я предлагаю альтернативное решение.
Если эта проблема описана — покажите описание. Пока его нет.
Ее вполне может решить.
Авторы статьи с вами не были согласны. Продемонстрируйте на испытаниях.
Почему не знаю? Вполне себе представляю.
Именно потому, что "представляете", а не знаете. Но проблема еще не возникла, возможно, не возникнет, или возникнет вообще не там. Я как-то долго и больно отлаживал перформанс десериализации XML, только чтобы выяснить, что ошибка была в одном неудачном
try...catch
(а совсем не там, где я думал, а думал я как раз на утечку памяти).Ascar Автор
21.05.2019 13:15Ээээ, какой такой «понятной причине»?
Первый объект создается когда хранилище пусто. Остальные — когда первый занял один объект, и одновременно оставшиеся потоки его запросят. Конечно при асинхронном занятии объектов из хранилища возможно больше экземпляров, вопрос в том правильная ли такая асинхронная логика.
Если эта проблема описана — покажите описание. Пока его нет.
Так уже не раз упомянулась из той же статьи. Или вы про что?
Авторы статьи с вами не были согласны. Продемонстрируйте на испытаниях.
Я не смогу продемонстрировать на их примере по понятным причинам, синтетический пример я проверял. Напишу еще раз: без пула множественное расположение в LOH и очистка, с ним — только 4 экземпляра.
Именно потому, что «представляете», а не знаете.
Паттерн «пул» давно придумали для решения описанной проблемы. Я предложил его имплементацию.
Но проблема еще не возникла, возможно, не возникнет, или возникнет вообще не там.
Пока гром не грянет, мужик не перекрестится
Я не смотрю так: не нужно тебе что то именно сейчас — не пиши.lair
21.05.2019 13:21Первый объект создается когда хранилище пусто. Остальные — когда первый занял один объект, и одновременно оставшиеся потоки его запросят.
Во-первых, у вас может быть больше одновременных потоков, чем процессоров. Во-вторых, а если у вас в норме нагрузка на два потока, а один раз за время жизни приложения случился пик — что вы выиграли?
Так уже не раз упомянулась из той же статьи. Или вы про что?
Проблема из той статьи — не ваша, и ваше решение к ней не подходит.
Напишу еще раз: без пула множественное расположение в LOH и очистка, с ним — только 4 экземпляра.
А синтетическая производительность?
Паттерн «пул» давно придумали для решения описанной проблемы.
Паттерн "пул", как и другие паттерны, предполагает формализованную задачу.
Я не смотрю так: не нужно тебе что то именно сейчас — не пиши.
Проблема в том, что ваше решение не бесплатно, у него есть накладные расходы. Поэтому его нельзя внедрять, не померяв, превышают ли выигрыш от решения эти расходы.
Ascar Автор
21.05.2019 13:32-1Во-первых, у вас может быть больше одновременных потоков, чем процессоров.
Потоки могут выполняться одновременно ограничиваясь числом ядер. 4 ядра — 4 контекста потока выполняются одновременно в ОС.
один раз за время жизни приложения случился пик
Если выходить за рамки вэба и где то в асинхронщине такое случится, что по мне это некая ошибка в логике по набирать таких объектов вне вэба, то надо думать о освобождении удалении объектов при снижении нагрузки, я писал про это.
Проблема из той статьи — не ваша, и ваше решение к ней не подходит.
Очевидно проблема той статьи не моя. Реализация пула может быть применима к чему угодно.
Поэтому его нельзя внедрять, не померяв, превышают ли выигрыш от решения эти расходы.
Так я не настаиваю, тут мы обсуждаем идеи.lair
21.05.2019 13:48Потоки могут выполняться одновременно ограничиваясь числом ядер. 4 ядра — 4 контекста потока выполняются одновременно в ОС.
Если у вас есть такое ограничение на входе. Его может и не быть.
надо думать о освобождении удалении объектов при снижении нагрузки
Вот, об этом и речь.
Реализация пула может быть применима к чему угодно.
Применить можно что угодно к чему угодно, вопрос в том, есть ли выигрыш.
Так я не настаиваю, тут мы обсуждаем идеи.
В перформансе намного осмысленнее обсуждать измерения. Идеи без измерений ничего не стоят.
Ascar Автор
21.05.2019 13:55-1Если у вас есть такое ограничение на входе. Его может и не быть.
Это принцип работы ОС. Допустим у вас 1000 потоков в одном процессе, но процессор имеет 4 ядра. Поэтому ОС сможет только переключатся в 4 контекста одновременно. Одновременно могут быть запрошены 4 объекта из хранилища.
Вот, об этом и речь.
Я рад что с N — го раза вы вчитались в мой текст.
Применить можно что угодно к чему угодно, вопрос в том, есть ли выигрыш.
На усмотрение потребителю.
В перформансе намного осмысленнее обсуждать измерения. Идеи без измерений ничего не стоят.
Элементарный тестовый пример с циклом в котом создаются объекты, и пример где используется пул. Пока это все что есть.lair
21.05.2019 14:01Поэтому ОС сможет только переключатся в 4 контекста одновременно. Одновременно могут быть запрошены 4 объекта из хранилища.
Вам не надо "одновременно", вам надо "до момента, как объекты отпущены". И если у вас тысяча потоков, то у вас может быть тысяча запрошенных объектов подряд.
На усмотрение потребителю.
Это называется "вы не знаете". О чем и речь.
Элементарный тестовый пример с циклом в котом создаются объекты, и пример где используется пул. Пока это все что есть.
То есть ничего совпадающего с реальной нагрузкой.
Ascar Автор
21.05.2019 14:08Вам не надо «одновременно», вам надо «до момента, как объекты отпущены». И если у вас тысяча потоков, то у вас может быть тысяча запрошенных объектов подряд.
Так я и писал про такой вариант развития. Надо думать про то как померить нагрузку и почистить пул.
Это называется «вы не знаете». О чем и речь.
Ну я не могу знать где будет использоваться библиотека, могу только описать ее и то что она делает, остальное на усмотрение.
То есть ничего совпадающего с реальной нагрузкой.
Ну реальная нагрузка сводится к этому, много созданий, много очисток. Я уже понял вашу логику: «пиши только о том с чем столкнулся в реальном проекте».lair
21.05.2019 14:11Надо думать про то как померить нагрузку и почистить пул.
Или сделать банальную верхнюю границу.
Ну реальная нагрузка сводится к этому, много созданий, много очисток.
… и совершенно не обязательно, что пул даст выигрыш. О чем и речь.
Ascar Автор
21.05.2019 14:14Или сделать банальную верхнюю границу.
Это уже конструктивнее.
… и совершенно не обязательно, что пул даст выигрыш. О чем и речь.
При определенных условиях, конечно не обязательно. Каждый инструмент для своей задачи.lair
21.05.2019 14:15При определенных условиях, конечно не обязательно.
И вот тут возникает вопрос: а в каких же условиях приведенная в посте реализация дает выигрыш? Только не умозрительный, а на конкретных измерениях.
retran
21.05.2019 14:56+1Ну реальная нагрузка сводится к этому, много созданий, много очисток.
Вы уверены, что «много созданий, много очисток» в clr медленнее пула в общем случае?Ascar Автор
21.05.2019 15:11Речь про большой объект, который в LOH упадет. Без пула он логически одноразовый но будет лежать до очистки, соответственно по достижении заданной квоты они будут очищаться(вместе со всей кучей), а это значит что все потоки приостановятся и это займет время. Если объект переиспользуемый то не будет множества его одноразовых копий. Как то так.
В общем случае GC быстро чистит и двигает указатели, там вроде бы даже есть фоновый режим.retran
21.05.2019 19:06Я не могу представить себе реальный сценарий.
Такой большой объект — это или очень большой массив, или массив структур больших, или буфер. Т. е. не те штуки на инициализации которых обычно можно сэкономить. Если это синглтон, то почему бы ему просто не быть синглтоном? Не понимаю.Ascar Автор
21.05.2019 19:41Если синглтон несколько задач возьмет придется его блокировать.
retran
21.05.2019 20:03Так вы для начала определитесь какую именно задачу вы решаете. Один объект на таск? Один объект на поток? Произвольное количество объектов на произвольное количество задач на произвольном количестве потоков?
retran
21.05.2019 19:55Потоки могут выполняться одновременно ограничиваясь числом ядер. 4 ядра — 4 контекста потока выполняются одновременно в ОС.
В вашем примере количество потоков ограничено не количеством процессоров, а размером пула потоков. Добавьте в ваши таски какой-нибудь Thread.Sleep(1000) и вы начнете получать случайное количество созданных объектов в пуле большее, чем количество процессоров.Ascar Автор
21.05.2019 19:57Это понятно, с await Task.Delay(1000) щас посмотрел. Верхнюю границу сделал кстати. Thread.Sleep может задедлочить.
retran
21.05.2019 20:02Thread.Sleep может задедлочить.
В каком месте? Thread.Sleep нужен чтобы гарантированно заблокировать поток, иначе количество взятых из пула потоков может и не превысить количество процессоров.
retran
21.05.2019 19:14Их число соответствует числу ядер процессора по понятной причине. Вы пример из исходников не смотрели.
Почитал пример из исходников. В этом примере пул не нужен, можно просто лениво класть ваш объект в ThreadLocal (на самом деле ConcurrentBag в вашей реализации ровно так и сделает).retran
21.05.2019 19:26Переписал ваш пример — gist.github.com/retran/0f47c5c239ddef876e3674b641da7574
Ascar Автор
21.05.2019 19:49Если задачи один поток выполняет, то объект разный или тот же? Еще для пула должна быть возможность задачами понабрать объектов.
retran
21.05.2019 19:59Тот же. Для задач есть AsyncLocal, который решает ту же задачу в контексте асинхронного кода, будет ровно один объект на один таск независимо от количества реальных потоков.
Ascar Автор
21.05.2019 20:02А потом что с ним происходит?
retran
21.05.2019 20:07С кем?
Ascar Автор
21.05.2019 20:08Ну с объектом. Идея его переиспользовать же.
retran
21.05.2019 20:11Переиспользовать как и с какими гарантиями? У меня по вашему примеру сложилось впечатление, что вы хотели один объект на один выполняющийся поток.
UPD… при произвольном количестве тасков. Это вполне может иметь смысл, если вы хотите что-то пооптимизировать за счет уменьшения contention между ядрами процессора.Ascar Автор
21.05.2019 20:21Крайний вариант это множественные таски забирают объекты и работают с ними.
retran
21.05.2019 20:28Я допускаю существование таких задач, но интуитивно есть ощущение, что если для множественных тасок понадобился общий пул, то где-то что-то не так. Хочется более реальный сценарий.
UPD общий пул объектов из LOHAscar Автор
21.05.2019 20:34если для множественных тасок понадобился общий пул, то где-то что-то не так
Согласен, я про это уже писал. Но как вариант должно и так работать.
Хочется более реальный сценарий.
В комментах поищите (не моих), где то было.
alhimik45
21.05.2019 12:23+2А какой сценарий у вас, что вам нужен пул объектов (не массивов), и вы имеете от него измеримый выигрыш?
Например, пул инстансов Хрома. Создавать его на каждую пришедшую задачу слишком долго, поэтому у нас реализован пул хромов, из которых демон берет первый свободный, рендерит всё, что надо, а потом возвращает его. Такая схема позволяет переживать кратковременные всплески нагрузки без увеличения latency, ценой лишней съедаемой памяти после всплеска (до следующего перезапуска демона при балансировке).
lair
21.05.2019 12:48О, спасибо за пример.
Любопытства ради — а у вас инстансы хрома живут в том же процессе, что и пул?..
alhimik45
21.05.2019 12:58Нет, он через
Process.Start
запускается.lair
21.05.2019 13:02Ну то есть у вас для очистки памяти надо целиком инстанс перезагрузить? Или у вас демон умеет при свой перезагрузке убивать процессы?
alhimik45
21.05.2019 13:14Демон умеет. При остановке у пула вызывается Dispose и если внутри него IDisposable, то он всех их диспозит, а обертка над процессом Хрома умеет его убивать.
retran
21.05.2019 13:19А какой сценарий у вас, что вам нужен пул объектов (не массивов), и вы имеете от него измеримый выигрыш?
Много контролов с очень коротким временем жизни, но сложной инициализацией и простой реинициализацией.
Игровые объекты в играх, классический пример — проджектайлы разных видов.
Справидливости ради, в LOH попадать здесь не должно ничего.lair
21.05.2019 13:22Много контролов с очень коротким временем жизни, но сложной инициализацией и простой реинициализацией.
А теперь читаем задачу из поста:
Нужно это для больших объектов которые по логике программы долго не должны храниться.
Совпало?
Ascar Автор
21.05.2019 13:39-1Там тоже время затрачивается как и при уборке 2 го поколения, чтоб снизить так же и эти временные затраты на инициализацию.
Taritsyn
21.05.2019 10:51А вот какие еще объекты вам так часто надо переиспользовать, что вы для этого сделали пул, и получили гарантированный измеримый выигрыш в используемых ресурсах?
Первое, что приходит в голову – это переиспользование экземпляров классаStringBuilder
.lair
21.05.2019 12:10Обычно в тех случаях, когда у вас
StringBuilder
жрет LOH, уже надо переставать конкатенировать строки и начать писать в поток.
Нет, не всегда. Но часто.
Taritsyn
21.05.2019 12:30В основном такие пулы требуются, чтобы снизить количество создаваемых экземпляров.
Taritsyn
21.05.2019 12:56Снижение использования LOH больше относится к случаям, когда мы работаем с большими массивами и в этих случаях нам хватает
ArrayPool
. АObjectPool
нам нужен для случаев, когда у нас создается много «тяжелых» объектов (например, экземпляров классовStringBuilder
иList
). Кстати, в реализации пулаStringBuilder
’ов на основе библиотеки Microsoft.Extensions.ObjectPool емкость удерживаемого в пуле экземпляра классаStringBuilder
ограничена 4096 символами.lair
21.05.2019 13:04В чем конкретно у вас тяжесть
StringBuilder
, и нельзя ли эту проблему решить отказом от него вовсе?
(я вполне серьезно спрашиваю, потому что в моих личных сценариях, где много сериализации/десериализации, я обычно иду именно по пути преобразования всего, что можно, в потоковую модель)
Taritsyn
21.05.2019 13:16+2Не всегда можно отказаться от использования
StringBuilder
‘ов. Например, команда ASP.NET Core не может сделать этого, поэтому они используют пулStringBuilder
’ов в ResponseCachingMiddleware.
retran
21.05.2019 13:14Ну вы бы хотя бы слабые ссылки прикрутили, что ли.
Зачем? Типичные сценарии использования пулов предполагают снижение нагрузки на gc и экономию на аллокациях (там где они дорогие) и инициализации за счет поедания памяти.
Вот только, да, если уж нужен пул, то надо очень аккуратно руками планировать его жизненный цикл, чего у автора нет.lair
21.05.2019 13:16Чтобы память жрать контролируемо. А то один скачок запросов — и у нас аллоцировано сто экземпляров, которые потом не нужны.
retran
21.05.2019 13:38Это иногда лучше, чем фриз на сборке мусора в самый неподходящий момент. Почистить можно руками, когда мы точно знаем, что скачок закончился (имхо, в тех сценариях где использование пулов разумно — это реально). Ну и обязательное ограничение размера пула сверху тоже никто не отменял (чего у автора, тоже, кстати нету).
lair
21.05.2019 13:50Это иногда лучше, чем фриз на сборке мусора в самый неподходящий момент.
Главное помнить, что пул может и увеличивать шансы на сборку мусора.
Ну и обязательное ограничение размера пула сверху тоже никто не отменял (чего у автора, тоже, кстати нету).
О том и речь, что у автора много чего нету.
Ascar Автор
21.05.2019 13:18Согласен с жизненным циклом, нужен механизм уменьшения объектов пула при снижении нагрузки, то есть надо как то измерить ее что ли. Возможно по таймеру.
Taritsyn
21.05.2019 10:46Вы пробовали универсальный пул от Microsoft — Microsoft.Extensions.ObjectPool?
lair
22.05.2019 00:21if (!_items.Contains(item))
Воу-воу, полный перебор пула на каждом возврате элемента? Причем не просто полный перебор, а еще и полное копирование с созданием нового
List<T>
? От лишних аллокаций хотели избавиться, да?Ascar Автор
22.05.2019 10:30Только лист, не самих объектов в них. А как вы предлагаете проверять? Без проверки тоже оставлять не хочется. Можно ConcurrentDictionary конечно попробовать. Либо в цикле пройтись. Что плохого в переборе то пула? Там не должно быть много элементов.
lair
22.05.2019 11:14Только лист, не самих объектов в них.
А это, типа, дешевая с точки зрения аллокации операция (особенно учитывая, что нынешняя реализация не задает capacity для
List
)?
А как вы предлагаете проверять?
Я лично предлагаю не писать велосипедов, пока не стало понятно, в каких условиях они будут использоваться. В данном конкретном случае мне, например, непонятно, зачем эта проверка вообще.
Что плохого в переборе то пула?
Аллокации (и сборка мусора), от которых вы, по идее, обещали избавиться, причем в неожиданный для пользователя момент.
Там не должно быть много элементов.
Это почему вдруг? У вас максимальное число элементов задается пользователем.
Ascar Автор
22.05.2019 11:17Проверка нужна для защиты от дублирования.
lair
22.05.2019 11:20А откуда у вас возьмется дублирование?
(вы же понимаете, да, что недобросовестный клиент может ваш пул испортить только в путь?)
Ascar Автор
22.05.2019 11:35Например Dispose продублируется, а флаг закешируется и будет двойной Return. В любом случае наличие дублей означает фатальную неисправность.
Я не пойму что там будет копироваться при переборе? И при Contains вы говорите о копировании? Не могли бы пояснить этот момент? Размер листа определен в Creator, но это размер объекта пула.lair
22.05.2019 12:25Например Dispose продублируется
Например, как? И не проще ли делать защиту от двойного
Dispose
внутри disposable, а не вReturn
?
В любом случае наличие дублей означает фатальную неисправность.
Тогда вам нужно множество с гарантированной уникальностью, а в этом случае ваши расходы резко вырастут. Вы все еще считаете, что ваш пул дает выигрыш?
Я не пойму что там будет копироваться при переборе?
А вы вообще в курсе, как работает
Contains
наConcurrentBag
?Ascar Автор
22.05.2019 12:43Например, как? И не проще ли делать защиту от двойного Dispose внутри disposable, а не в Return?
Там есть проверка, можно volatile добавить, но в Return понадежнее, у нас не только Dispose возвращает, а и сам метод.
Тогда вам нужно множество с гарантированной уникальностью
ConcurrentDictionary тоже осуществляет проверку. Пул даст выигрыш если его по назначению использовать.
А вы вообще в курсе, как работает Contains на ConcurrentBag?
Так я у вас поинтересовался, вы сказали, теперь поясните. Про простой перебор в цикле тоже, что там за аллокации.lair
22.05.2019 13:27в Return понадежнее, у нас не только Dispose возвращает, а и сам метод.
… и как у вас
Return
может быть вызван дважды для одного объекта?
ConcurrentDictionary
тоже осуществляет проверку.И приносит дополнительные накладные расходы. Собственно, он частный случай "множества с гарантированной уникальностью".
Так я у вас поинтересовался, вы сказали, теперь поясните.
Ну то есть не знаете. Окей, краткий экскурс в
new ConcurrentBag<object>().Contains("t")
:
У
ConcurrentBag<T>
нет методаContains
. Следовательно что? Правильно, вызываетсяEnumerable.Contains
.
Поскольку
ConcurrentBag<T>
не реализуетICollection<T>
, происходит банальное:
var comparer = EqualityComparer<T>.Default foreach (TSource element in source) if (comparer.Equals(element, value)) return true; return false;
Кстати, полезно подумать, почему использование дефолтного компарера — плохо.
Теперь нас интересует
ConcurrentBag<T>.GetEnumerator
. В документации написано: "The enumeration represents a moment-in-time snapshot of the contents of the bag". В принципе, этой фразы достаточно, чтобы понять наши проблемы, но давайте посмотрим в исходник, который за вычетом проверок и блокировок сводится к одной интересующей нас строчке:return ToList().GetEnumerator();
.
Посмотрим в
ToList
:
List<T> list = new List<T>(); while (проход_по_линкед_листу) list.Add(currentNode.m_value);
Теперь понятно, что копируется?
Про простой перебор в цикле тоже, что там за аллокации.
Простой перебор в цикле чего?
Ascar Автор
22.05.2019 13:56Как и подозревал создается только List и еще некоторые объекты для потокобезопасности, но объекты пула те же из m_headList. За точную инфу конечно спасибо.
Этот лист даже если элементов 1000 где по вашему будет размещен в куче?
В принципе у меня стоит блокировка, можно теперь и циклом for заменить.lair
22.05.2019 13:58Этот лист даже если элементов 1000 где по вашему будет размещен в куче?
Там же, где и (по умолчанию) те объекты, которые вы пытаетесь сложить в пул. То есть это те же самые аллокации, которых вы хотите избежать.
Я надеюсь, вы помните, как работает
List.Add
?
В принципе у меня стоит блокировка можно теперь и циклом for заменить.
… и что изменится? Кстати, как вы сделаете цикл с
for
поConcurrentBag<T>
?Ascar Автор
22.05.2019 14:01Там же, где и (по умолчанию) те объекты, которые вы пытаетесь сложить в пул.
Там там то предположительно LOH, а лист в 0 отправится, разница есть.
… и что изменится?
Как что? Не будет обращение к тем классам по вашей ссылке и удорожания операции.lair
22.05.2019 14:03Там там то предположительно LOH
Нет там никакого "предположительно LOH", все приведенные в этом посте сценарии никакого отношения к LOH не имеют. У вас, напоминаю, универсальный пул.
Не будет обращение к тем классам по вашей ссылке и удорожания операции.
И как же вам это удастся? Покажите код.
Ascar Автор
22.05.2019 14:08Нет там никакого «предположительно LOH», все приведенные в этом посте сценарии никакого отношения к LOH не имеют. У вас, напоминаю, универсальный пул.
Вообще то есть, так же объекты которые долго инициализировать и быстрее обнулить, иначе пул не надо использовать, пишу уже не первый раз вам это…
И как же вам это удастся? Покажите код.
Как напишу — скину.lair
22.05.2019 14:16так же объекты которые долго инициализировать и быстрее обнулить
А при чем тут LOH? Вон там недалеко приводят в пример
StringBuilder
, они точно не ложатся в LOH, потому что ограничены 4096 символами.
иначе пул не надо использовать
Чем дальше мы это обсуждаем, тем больше выясняется, что сценариев, когда ваш пул надо использовать, все меньше и меньше.
Ascar Автор
22.05.2019 14:27А при чем тут LOH? Вон там недалеко приводят в пример StringBuilder, они точно не ложатся в LOH, потому что ограничены 4096 символами.
Значит операция по его созданию дорога. Я не вижу никакой проблемы когда короткоживущий List по вашей ссылке будет подчищен GC, все как и с остальными объектами.
Чем дальше мы это обсуждаем, тем больше выясняется, что сценариев, когда ваш пул надо использовать, все меньше и меньше.
Я уже написал для чего это и откуда пришла идея. Как оказалось есть и другие реализации. Вы будите повторять одно и тоже пока с вами не согласятся? По моему это так не работает…lair
22.05.2019 14:30Значит операция по его созданию дорога.
Дорога аллокация.
Я не вижу никакой проблемы когда короткоживущий List по вашей ссылке будет подчищен GC, все как и с остальными объектами.
А я вот не видел проблемы с тем, чтобы создавать
StringBuilder
, а у кого-то они есть. Это все говорит нам ровно об одном: нет никакого "общего универсального решения", есть конкретный сценарий, в котором нужно мерять влияние. А у вас нет ни сценария, ни измерений.
Как оказалось есть и другие реализации.
… но вы вместо того, чтобы взять готовую протестированную реализацию, все равно пишете свое, гуляя по всем мыслимым граблям.
Ascar Автор
22.05.2019 16:50… но вы вместо того, чтобы взять готовую протестированную реализацию, все равно пишете свое, гуляя по всем мыслимым граблям.
Я могу что угодно начать писать. Кто угодно может начать писать что угодно. Ловить постоянно бугурты от этого — дело ваше.
Вы ищите решение согласно задаче, берете готовый обкатанный инструмент, и это логично. Цель этой статьи совпадает с вашей?retran
22.05.2019 17:00Вы ищите решение согласно задаче, берете готовый обкатанный инструмент, и это логично. Цель этой статьи совпадает с вашей?
Проблема в том, что не очень опытный человек вобьет в гугл ключевые слова, найдет вашу статью… и огребет проблем.Ascar Автор
22.05.2019 17:04Так погуглите. Скорее всего он найдет.
retran
22.05.2019 17:07Так погуглите.
Мне лень. А вы действительно погуглите и разберитесь как оно устроено и почему.Ascar Автор
22.05.2019 17:09как оно устроено и почему.
Что устроенно? Я же скинул популярное расширение.lair
22.05.2019 22:29Вот ровно то расширение, которое вы скинули, и устроено. Там есть много полезных ответов, включая вопрос "что делать с двойным возвратом", "как минимизировать блокировки" и так далее.
Ascar Автор
22.05.2019 17:10Taritsyn
22.05.2019 21:10Еще есть хорошая реализация пула объектов из проекта Roslyn, которая не попала в эту выдачу, потому что не представлена в виде NuGet-пакета.
lair
22.05.2019 21:38Я могу что угодно начать писать. Кто угодно может начать писать что угодно.
Тут вопрос не в том, что писать, а в том, что публиковать. Вы, конечно, можете публиковать, что вам пропускают, ну а мы можем комментировать в меру своих желаний.
lair
22.05.2019 14:46+1но в Return понадежнее, у нас не только Dispose возвращает, а и сам метод.
Нет-нет, подождите-подождите, давайте все-таки вернемся к вопросу: в какой ситуации у вас
Return
может быть вызван дважды для одного объекта?Ascar Автор
22.05.2019 16:19-1Не у меня, у пользователя.
lair
22.05.2019 16:20Нет, не "у пользователя".
В каком случае метод
Return
предлагаемого вами класса может быть вызван дважды, в каком конкретно допустимом сценарии использования?Ascar Автор
22.05.2019 16:50В допустимом очевидно его один раз надо вызывать.
lair
22.05.2019 21:49Тогда зачем же вы тратите ресурсы на случай, который никак не проявится у того, кто использует ваш пул правильно, и при этом никак не показываете тому, кто использует ваш пул неправильно, что он совершил ошибку? В этом нет вообще никакого смысла.
lair
22.05.2019 13:40+1Кстати, если уж на то пошло,
Count
— тоже блокирующая операция со стоимостью O(n).
AgentFire
itemCreator = itemCreator ?? throw new ArgumentNullException("itemCreator");
Если уж использовать фишки шарпа, то не стоит забывать и про
nameof
Count
в данном случае больше подходит в виде свойства, т.к. не производит никаких вычислений.IDisposable
обертку:_items
не изменяется, стоит задуматься о модификатореreadonly
.AgentFire
Я смотрю, пул был немного доработан. Вот, что я об этом думаю:
SpinLock
. Разработан для ситуаций, когда используется быстрая или очень быстрая работы системы, когда важна производительность. Следует ли задуматься о создании метода-перегрузки для медленной работы, оно же — асинхронной?SpinLock
. Судя по этой статье, он использует тот жеMonitor
, из чего следует, что удобнее и проще его заменить на уже готовый синтактический сахар в виде блокаlock()
.Return
весьма неоднозначно определяет для конечного пользователя, будет ли возвращенный объект "деструктурирован". В данном случае у тебя не вызывается_itemClearer
, если достигнута верхняя граница. Стоит подумать об однозначности и понятности методаReturn
. Как вариант, использоватьbool (Try)Return
, возвращая пользователю информацию о том, будет ли объект переиспользован, или же он будет собран сборщиком, как лишний.Pooled<T>
на структуру, в контексте высокопроизводительного приложения, коим ты наградил свое изобретение, и подвести итог, что же все-таки лучше. Сам не знаю эту часть настолько глубоко.retran
Простите, а что по вашему такое SpinLock?
AgentFire
вах, виноват, не использует он
Monitor
, не прокатило, вычеркиваем!retran
Нет, это мьютекс использует внутри спинлок.
И, да, тут надо написать «не используйте спинлоки если точно не понимаете что и зачем делаете, используйте lock { }, там уже за вас подумали, а если понимаете — то скорее всего понимаете как написать пул на CAS без блокировок».
AgentFire
mutex использует winapi, не?
retran
И Monitor, и Mutex — мьютексы (и спинлок — тоже, но как-то принято их не смешивать с "обычными"). Мне, конечно, стоило уточнить, что я имею в виду Monitor во избежание путаницы, прошу прощения.
Ascar Автор
SpinLock блокирует на пользовательском уровне, зацикливанием.
retran
А Monitor?
Ascar Автор
Гибрид
lair
Дадада, сначала давайте возьмем конкурентную коллекцию, а потом давайте обернем обращение к ней в блокировку. Круто же, чего.
Ascar Автор
Спасибо за коммент. Я думал изначально над асинхронным ожиданием, но у нас нет длительных операций, блокировка на время проверки, очистки и добавки в коллекцию, по моему оно того не стоит. Продолжения со стейтмашинами так же память занимают.
3 — ок.
4 — структуру вместе с using не получится, компилятор делает защитную копию.
lair
То есть на самую длительную подконтрольную вам операцию, да. На этом фоне непонятно, зачем вам вообще
ConcurrentBag
.