Да, единый реестр исключений это не только более эффективно по производительности, но и более красиво.
Обычно я создаю статический класс Errors и помещаю туда все выбрасываемые исключения. Код, который создаёт текст исключения, закономерно переходит туда же. В результате мы получаем более чистый код в месте выбрасывания исключения.
В принципе, ConcurrentDictionaryда, быстрее для случаев многопоточности, но есть способ сомнительный и не безопасный способ ускориться с Dictionary: вроде как ставить lock только при записи. Если найду пример и результаты бенчмарков - скину.
Спасибо! Я добавлю ваш комментарий в статью. Однако, я должен отметить, что мои измерения несколько иные. Действительно "рвёт" только в .NET Core 3.1. Код обновил.
Только, во-первых, надо использовать не DoWork, а сделать переменную и туда положить DoWork, иначе будет аллокация, о чём честно предупреждает Rider.
Во-вторых, необходимо всё-таки выполнить условия задачи, совместив объект данных с handler'ом. Для этого я обновил бенчмарк и сделал объект TaskFactoryClosure. Я понимаю, что из этого синтетического теста не очень понятно, что надо совместить данные + обработчик, но представим себе, что они у вас разные и формируются по разному под данные. Из теста это исключено, чтобы не замерять бизнес-логику и сконцентрироваться на аллокации. Статья-то про это)
Ну и вот результаты: плюс-минус аналогичные SelfClosure. Круто!
Может быть, в таком случае есть смысл сделать какую-то свою очередь/пул задач
Вы прям описали одну известную библиотеку для background-обработки задач. Для случаев "fire and forget" она подходит идеально и построена примерно так, как вы написали.
В моём случае понадобилось небольшое вкрапление (микрооптимизация) в горячем месте кода. Что-то вроде вот такого, что я писал для Mediator (использовать в продакшене не рекомендую!).
Также, не пробовали ридонли структуры для своих замыканий?
В них же нужно запихивать значение. Либо пересоздавать всю структуру каждый раз, при каждом вызове. Если будет время, то попробую, спасибо.
Во-первых, потому что я могу это сделать и это действительно будет работать именно так. В реальном приложении я, опуская детали, точно также беру заранее подготовленный набор замыканий через Interlocked.Exchange. Если он null, я создаю новый массив с замыканиями. После использования, я кладу массив обратно. Короче говоря, в самом плохом сценарии получаю плюс-минус тот же результат, что и в AutoClosure.
Во-вторых, а зачем, собственно, мне создавать массив с замыканиями на каждый запрос? Зачем мне вообще создавать объект замыканий, если я могу их предсоздать и запулить. Если бы я назвал это Pool, было бы проще? Воспринимайте это как пул замыканий (а-ля вот так), сильно упрощённый для теста.
В-третьих, для Parallel.ForEach я тоже заранее создаю набор "замыканий". От этого ничего не меняется.
Очень хороший вопрос, который я забыл осветить в статье!
Действительно, использование Parallel.For и Parallel.Foreach значительно повышает скорость работы. Однако, к сожалению, их использование существенно увеличивают аллокацию (почти в три раза выше на .NET 6):
Дьявол, к сожалению, кроется в деталях. Parallel.Foreach принимает в качестве аргумента IEnumerable<T>, который возвращает IEnumerator<T>. Объект, который будет расположен в куче. Parallel.For снова создаёт то самое замыкание, что также влияет на аллокацию.
Я объяснюсь. При написании статьи бывает чертовски сложно балансировать на определённом уровне знаний, которые, волей-неволей, предъявляются к читателю. Детальное разжевывание мелочей имплементации захламляет статью и отпугивает знатоков. Увы, это и не привлекает людей уровня junior, так как для них это просто не интересно. У них задача "сделать", а не "понятно как сделать, но нужно, чтобы работало быстрее".
Да, к сожалению, вот так. Я не знаю о чем думали создатели этого механизма и может быть есть более оптимальный способ (знатоки подскажут)... но я использовал наиболее простой и самый распространенный способ создания Task'a. Возможно, была надежда на то, что получится короткоживущий объект и он будет удалён из кучи почти сразу. Но это, к сожалению, не так и бенчмарк это подтверждает - "объекты замыкания" существуют в Gen2.
Скажите, а код, который сейчас выложен в GitHub (ссылка и код, который сейчас вы предлагаете скачать с Я.Диска в качестве архива — это одинаковый код? Очень не удобно скачивать архивы, но посмотреть код было бы очень интересно.
Да, мне тоже очень нравится, но только для простых задач. Например, парсить и создавать в функции из JavaScript — ад. Там возникает сразу целая куча проблем вроде того, что ExpressionTrees иногда сложно составлять «по ходу» кода. Иногда нужно заглянуть на пару шагов вперед, чтобы выражение получилось правильное.
Я, наверно, вас расстрою. Бегло посмотрев код я заподозрил, что данный [де]сериализатор будет очень много аллоцировать. Собственно, так оно и оказалось. Я обновил бенчмарки сериализации и десериализации.
Я скопировал файл SimpleJson и добавил его в проект. Ничего не менял.
При такой аллокации его использовать просто опасно. В бенчмарке всего 10 000 элементов, которые де/сериализуются. Тратить на такую простую операцию 600 мегабайт — очень сомнительное удовольствие.
Да, единый реестр исключений это не только более эффективно по производительности, но и более красиво.
Обычно я создаю статический класс
Errors
и помещаю туда все выбрасываемые исключения. Код, который создаёт текст исключения, закономерно переходит туда же. В результате мы получаем более чистый код в месте выбрасывания исключения.Да, верное замечание.
В принципе,
ConcurrentDictionary
да, быстрее для случаев многопоточности, но есть способ сомнительный и не безопасный способ ускориться с Dictionary: вроде как ставить lock только при записи. Если найду пример и результаты бенчмарков - скину.Спасибо! Добавил в статью и обновил бенчмарк.
Пример: написание тиражируемых библиотек. Например, класс List<T> в базовой библиотеке .NET. Или, например, RavenDB.
Спасибо! Я добавлю ваш комментарий в статью. Однако, я должен отметить, что мои измерения несколько иные. Действительно "рвёт" только в .NET Core 3.1. Код обновил.
Окружение:
Результаты:
Спасибо большое за замечание! Ссылку обновил - https://gist.github.com/teoadal/9297c0b574a175fc295bb29c01782fa2
Возможно. Код не приложил, извините - за давностью лет немного потерялся. Попробую восстановить.
Да, всё верно. Можно и так.
Только, во-первых, надо использовать не DoWork, а сделать переменную и туда положить DoWork, иначе будет аллокация, о чём честно предупреждает Rider.
Во-вторых, необходимо всё-таки выполнить условия задачи, совместив объект данных с handler'ом. Для этого я обновил бенчмарк и сделал объект TaskFactoryClosure. Я понимаю, что из этого синтетического теста не очень понятно, что надо совместить данные + обработчик, но представим себе, что они у вас разные и формируются по разному под данные. Из теста это исключено, чтобы не замерять бизнес-логику и сконцентрироваться на аллокации. Статья-то про это)
Ну и вот результаты: плюс-минус аналогичные SelfClosure. Круто!
О, спасибо большое! Я обязательно добавлю это в статью и доработаю бенчмарк. Но я всё ещё рад, что SelfClosure обходит AutoClosure по аллокации)
Вот за это я люблю Хабр! Комменты от профессионального сообщества всегда интереснее и полезнее, чем на всяких других площадках.
Вы прям описали одну известную библиотеку для background-обработки задач. Для случаев "fire and forget" она подходит идеально и построена примерно так, как вы написали.
В моём случае понадобилось небольшое вкрапление (микрооптимизация) в горячем месте кода. Что-то вроде вот такого, что я писал для Mediator (использовать в продакшене не рекомендую!).
В них же нужно запихивать значение. Либо пересоздавать всю структуру каждый раз, при каждом вызове. Если будет время, то попробую, спасибо.
Во-первых, потому что я могу это сделать и это действительно будет работать именно так. В реальном приложении я, опуская детали, точно также беру заранее подготовленный набор замыканий через
Interlocked.Exchange
. Если он null, я создаю новый массив с замыканиями. После использования, я кладу массив обратно. Короче говоря, в самом плохом сценарии получаю плюс-минус тот же результат, что и в AutoClosure.Во-вторых, а зачем, собственно, мне создавать массив с замыканиями на каждый запрос? Зачем мне вообще создавать объект замыканий, если я могу их предсоздать и запулить. Если бы я назвал это Pool, было бы проще? Воспринимайте это как пул замыканий (а-ля вот так), сильно упрощённый для теста.
В-третьих, для Parallel.ForEach я тоже заранее создаю набор "замыканий". От этого ничего не меняется.
Простите, а почему вы так считаете?
Ничего же не изменилось: замыкание существует. Ваш результат - AutoClosureWhat.
Очень хороший вопрос, который я забыл осветить в статье!
Действительно, использование Parallel.For и Parallel.Foreach значительно повышает скорость работы. Однако, к сожалению, их использование существенно увеличивают аллокацию (почти в три раза выше на .NET 6):
Дьявол, к сожалению, кроется в деталях. Parallel.Foreach принимает в качестве аргумента IEnumerable<T>, который возвращает IEnumerator<T>. Объект, который будет расположен в куче. Parallel.For снова создаёт то самое замыкание, что также влияет на аллокацию.
Да, вы правы.
Я объяснюсь. При написании статьи бывает чертовски сложно балансировать на определённом уровне знаний, которые, волей-неволей, предъявляются к читателю. Детальное разжевывание мелочей имплементации захламляет статью и отпугивает знатоков. Увы, это и не привлекает людей уровня junior, так как для них это просто не интересно. У них задача "сделать", а не "понятно как сделать, но нужно, чтобы работало быстрее".
Да, к сожалению, вот так. Я не знаю о чем думали создатели этого механизма и может быть есть более оптимальный способ (знатоки подскажут)... но я использовал наиболее простой и самый распространенный способ создания Task'a. Возможно, была надежда на то, что получится короткоживущий объект и он будет удалён из кучи почти сразу. Но это, к сожалению, не так и бенчмарк это подтверждает - "объекты замыкания" существуют в Gen2.
Отличная статья, спасибо! Очень мотивирует менять себя, стремиться к мечте и достигать её.
Скажите, а код, который сейчас выложен в GitHub (ссылка и код, который сейчас вы предлагаете скачать с Я.Диска в качестве архива — это одинаковый код? Очень не удобно скачивать архивы, но посмотреть код было бы очень интересно.
Десериализация:
Newtonsoft: 1 (аллокация 35.47 MB)
SimpleJson: 1.81 (аллокация 667.02 MB)
Velo: 0.43 (аллокация 12.36 MB)
Сериализация:
Newtonsoft: 1 (аллокация 25.44 MB)
SimpleJson: 1.15 (аллокация 72 MB)
Velo: 0.39 (аллокация 5.93 MB)
Я скопировал файл SimpleJson и добавил его в проект. Ничего не менял.
При такой аллокации его использовать просто опасно. В бенчмарке всего 10 000 элементов, которые де/сериализуются. Тратить на такую простую операцию 600 мегабайт — очень сомнительное удовольствие.
Я нигде не храню токены. JsonTokenizer специально построен как IEnumerator — токены считываются из исходной строки сразу и передаются потребителю.