Pull to refresh
31
0
Кирилл @teoadal

Senior .NET Developer

Send message

Да, единый реестр исключений это не только более эффективно по производительности, но и более красиво.

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

Да, верное замечание.

В принципе, ConcurrentDictionaryда, быстрее для случаев многопоточности, но есть способ сомнительный и не безопасный способ ускориться с Dictionary: вроде как ставить lock только при записи. Если найду пример и результаты бенчмарков - скину.

|            Method |            Runtime |      Mean | Ratio |
|------------------ |------------------- |----------:|------:|
|        Dictionary |           .NET 6.0 |  7.774 us |  1.00 |
| DictionaryMarshal |           .NET 6.0 |  3.763 us |  0.49 |
|          Glossary |           .NET 6.0 |  2.007 us |  0.26 |

Спасибо! Добавил в статью и обновил бенчмарк.

Пример: написание тиражируемых библиотек. Например, класс List<T> в базовой библиотеке .NET. Или, например, RavenDB.

Спасибо! Я добавлю ваш комментарий в статью. Однако, я должен отметить, что мои измерения несколько иные. Действительно "рвёт" только в .NET Core 3.1. Код обновил.

Окружение:

BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1265/22H2/2022Update/SunValley2)
AMD Ryzen 7 5800H with Radeon Graphics, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.102
  [Host]             : .NET 6.0.13 (6.0.1322.58009), X64 RyuJIT AVX2
  .NET 6.0           : .NET 6.0.13 (6.0.1322.58009), X64 RyuJIT AVX2
  .NET Core 3.1      : .NET Core 3.1.22 (CoreCLR 4.700.21.56803, CoreFX 4.700.21.57101), X64 RyuJIT AVX2
  .NET Framework 4.8 : .NET Framework 4.8.1 (4.8.9139.0), X64 RyuJIT VectorSize=256

Результаты:

|        Method |                Job |            Runtime |     Mean |    Error |   StdDev | Ratio | RatioSD |
|-------------- |------------------- |------------------- |---------:|---------:|---------:|------:|--------:|
|           For |           .NET 6.0 |           .NET 6.0 | 504.5 ns |  2.37 ns |  2.22 ns |  2.00 |    0.03 |
|       Foreach |           .NET 6.0 |           .NET 6.0 | 252.3 ns |  3.69 ns |  3.08 ns |  1.00 |    0.00 |
| ForeachCustom |           .NET 6.0 |           .NET 6.0 | 254.8 ns |  2.63 ns |  2.46 ns |  1.01 |    0.02 |
|   ForeachSpan |           .NET 6.0 |           .NET 6.0 | 252.3 ns |  3.82 ns |  3.39 ns |  1.00 |    0.02 |
|        Unsafe |           .NET 6.0 |           .NET 6.0 | 260.3 ns |  3.01 ns |  2.67 ns |  1.03 |    0.02 |
|   UnsafeFixed |           .NET 6.0 |           .NET 6.0 | 251.6 ns |  3.88 ns |  3.24 ns |  1.00 |    0.02 |
|               |                    |                    |          |          |          |       |         |
|           For |      .NET Core 3.1 |      .NET Core 3.1 | 516.3 ns | 10.07 ns | 10.34 ns |  1.02 |    0.03 |
|       Foreach |      .NET Core 3.1 |      .NET Core 3.1 | 505.6 ns |  5.50 ns |  5.14 ns |  1.00 |    0.00 |
| ForeachCustom |      .NET Core 3.1 |      .NET Core 3.1 | 503.9 ns |  5.28 ns |  4.94 ns |  1.00 |    0.01 |
|   ForeachSpan |      .NET Core 3.1 |      .NET Core 3.1 | 252.4 ns |  2.86 ns |  2.67 ns |  0.50 |    0.01 |
|        Unsafe |      .NET Core 3.1 |      .NET Core 3.1 | 261.4 ns |  2.78 ns |  2.60 ns |  0.52 |    0.01 |
|   UnsafeFixed |      .NET Core 3.1 |      .NET Core 3.1 | 251.8 ns |  2.13 ns |  1.99 ns |  0.50 |    0.01 |
|               |                    |                    |          |          |          |       |         |
|           For | .NET Framework 4.8 | .NET Framework 4.8 | 506.1 ns |  7.55 ns |  7.07 ns |  1.99 |    0.02 |
|       Foreach | .NET Framework 4.8 | .NET Framework 4.8 | 253.7 ns |  1.96 ns |  1.73 ns |  1.00 |    0.00 |
| ForeachCustom | .NET Framework 4.8 | .NET Framework 4.8 | 437.6 ns |  8.57 ns |  8.80 ns |  1.72 |    0.04 |
|   ForeachSpan | .NET Framework 4.8 | .NET Framework 4.8 | 760.1 ns |  9.50 ns |  8.89 ns |  3.00 |    0.04 |
|        Unsafe | .NET Framework 4.8 | .NET Framework 4.8 | 505.0 ns |  3.33 ns |  2.95 ns |  1.99 |    0.02 |
|   UnsafeFixed | .NET Framework 4.8 | .NET Framework 4.8 | 252.2 ns |  2.79 ns |  2.61 ns |  0.99 |    0.01 |

Спасибо большое за замечание! Ссылку обновил - 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 (ссылка и код, который сейчас вы предлагаете скачать с Я.Диска в качестве архива — это одинаковый код? Очень не удобно скачивать архивы, но посмотреть код было бы очень интересно.
Да, мне тоже очень нравится, но только для простых задач. Например, парсить и создавать в функции из JavaScript — ад. Там возникает сразу целая куча проблем вроде того, что ExpressionTrees иногда сложно составлять «по ходу» кода. Иногда нужно заглянуть на пару шагов вперед, чтобы выражение получилось правильное.
Я, наверно, вас расстрою. Бегло посмотрев код я заподозрил, что данный [де]сериализатор будет очень много аллоцировать. Собственно, так оно и оказалось. Я обновил бенчмарки сериализации и десериализации.

Десериализация:
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 — токены считываются из исходной строки сразу и передаются потребителю.

Information

Rating
Does not participate
Location
Нижний Новгород, Нижегородская обл., Россия
Registered
Activity

Specialization

Backend Developer, Fullstack Developer
Lead
SQL
C#
ASP.NET MVC
Linq
.NET
ASP.Net
PostgreSQL