Как стать автором
Обновить
28
-3
Кирилл @teoadal

Senior .NET Developer

Отправить сообщение

Там точно ошибка.

В бенчмарке For есть надпись i < Items.Length. Items в данном бенчмарке это статическое свойство, которое вычисляется при обращении к нему. То есть при каждой итерации по циклу мы каждый раз дергаем свойство Items где каждый раз заново создается массив у которого берется Length.

Отсюда такая бешенная аллокация.

Отмечу ещё, что автор статьи не бежит по массиву, а просто инкрементирует i и приплюсовывает результат к sum. То есть он не обращается к содержимому массива вообще. В деле поиска подстроки это поможет вряд ли, так как строку для разделения всё-таки надо читать.

Спасибо вам больше! Мне было очень интересно. Жаль, что статья не получила продолжения!

Если они сделают как обещали (оптимизация по чтению), то я уверен, что будет быстрее. Но мы, конечно, перепроверим после выхода net8.

Foreach не генерит мусор, если Enumerator это struct. Вроде как struct enumerator'ы реализованы уже для всех основных коллекций.

Если же вы бежите по IList или другим интерфейсам коллекций, то да, struct enumerator будет упакован и будет аллокация.

Про Linq согласен. Linq в "горячем месте кода" надо разворачивать в foreach.

Да, для 99% случаев я в обычном коде использую ConcurrentDictionary. Это сильно проще, чем городить высокопроизводительный код, который трудно поддерживать.

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

Там же не очередь, насколько я помню. Где-то в видео от коллег из JetBrains было, что они столкнулись с подобным, и им пришлось написать свой ReadWriteLockSlim, но с очередью.

Ого! Спасибо, я попробую. С SIMD обращусь к специалисту, у меня с ним нет опыта.

Давайте говорить предметно.

для кода...

1. О каком коде мы говорим? Давайте вы изложите его в виде того самого кода, который у вас в голове. Решение должно быть быстрее ConcurrentDictionary и потреблять меньше памяти, подтвердите это бенчмарками.
2. Потом мы его обсудим.
3. Потом мы его сравним с тем способом, который я не помню.
4. Убедимся, что это не тот способ.
5. Согласимся, что ваш способ ошибочен и так поступать не нужно.

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

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

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

Вы к чему ведёте-то в рамках темы?

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

Мне кажется, что вы неправы. Моё мнение основывается на следующих шагах:

1. Берём любимый поисковик, вбиваем: "C# thread-safe mutex".
2. Убеждаемся, что mutex множество раз употребляется в контексте потокобезопасности.
3. Идём на github в код mutex'a. Вот сюда, например.
4. В открывшемся файле находим ключевое слово unsafe.

Вывод, который я делаю: слово "небезопасный" употребляется в случае потокобезопасности и не означает "неработающий".

Коллега, странный спор, так как я не понимаю, к чему вы ведёте и какую мысль отстаиваете.

Отвечу по существу:
1. Я написал "вроде как". Я помню, что там был какой-то трюк, но вспомнил только lock-free.

2. Я разве писал про то, что код не будет работать в каких-то случаях? Я написал только про то, что мне помнится, что он был сомнительный и небезопасный. Есть разница между словом "небезопасный" и "неработающий".

3. Мне кажется, что у вас в голове сформировалось какая-то реализация, и вы предположили, что я имею ввиду именно эту реализацию. Почему? Я же чётко написал, что я её даже не помню.

Я так сказал?

Мне кажется, что видел что-то вроде вот этого из RavenDB: своя имплементация lock-free dictionary.

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

Как мне кажется, больше граблей, чем пользы

Я начну комментировать с конца вашего сообщения, так как я полностью согласен с этим утверждением. Вообще, заниматься производительностью это грабли, боль и унижение. Тем не менее, есть 1% случаев, когда это действительно нужно.

Посмотрим, что мы экономим на struct-словаре. Ровно одну аллокацию

Нет, мы экономим на каждом вызове методов словаря. И это только в контексте нашего обсуждения по call/callvirt. См. бенчмарки.

Одной аллокацией больше, одной меньше — погоды не делает.

Мне кажется, что вы рассуждаете с позиции, что для 99% разработчиков одна аллокация погоды не сделает. И это правда. Но есть случаи, когда просто необходима высокая производительность и насколько можно низкая аллокация. Даже за счёт экономии на спичках.

Очень большая вероятность забыть

Раньше я в самом начале своих статей писал более длинное и более развернутое предупреждение о том, что подобные эксперименты - только для специалистов и только для узкого круга случаев применения. В этот раз я ограничился фразой: 99% программистов этого не нужно, а подобные эксперименты без изучения environment'a будут даже опасны.

Я ещё раз скажу, что заниматься производительностью это грабли, боль и унижение. Прежде чем использовать подобные штуки в проде надо подумать миллион раз, потом миллион раз перепроверить, да ещё и тесты с бенчмарками написать. В этом случае вероятность ошибиться есть, но она крайне низка.

CallVirt в классе без sealed
CallVirt в классе без sealed
CallVirt в запечатанном классе
CallVirt в запечатанном классе
Call в структуре
Call в структуре

Мы, вроде, с вами читаем одни и те же вещи, но понимаем их по разному. Я ещё раз напомню, что моё утверждение состоит в следующем: использовать структуры для увеличения производительности это хороший способ избежать callvirt.

Вот я взял и создал класс Glossary, скопировал в него код структуры, запечатал. Создал такой же класс, но не запечатывал его. Смотрим измерений производительности. Как и предсказывалось: структура быстрее, за счёт того, что есть call, а не callvirt.

|                       Method |            Runtime |     Mean | Ratio |
|----------------------------- |------------------- |---------:|------:|
|        DictionaryTryGetValue |           .NET 6.0 | 4.085 us |  1.00 |
|          GlossaryTryGetValue |           .NET 6.0 | 1.741 us |  0.43 |
| GlossaryNonSealedTryGetValue |           .NET 6.0 | 2.551 us |  0.63 |
|    GlossarySealedTryGetValue |           .NET 6.0 | 2.499 us |  0.61 |
|                              |                    |          |       |
|        DictionaryTryGetValue |           .NET 7.0 | 3.768 us |  1.00 |
|          GlossaryTryGetValue |           .NET 7.0 | 1.510 us |  0.40 |
| GlossaryNonSealedTryGetValue |           .NET 7.0 | 2.638 us |  0.70 |
|    GlossarySealedTryGetValue |           .NET 7.0 | 2.617 us |  0.70 |
|                              |                    |          |       |
|        DictionaryTryGetValue |      .NET Core 3.1 | 4.965 us |  1.00 |
|          GlossaryTryGetValue |      .NET Core 3.1 | 2.534 us |  0.51 |
| GlossaryNonSealedTryGetValue |      .NET Core 3.1 | 2.526 us |  0.51 |
|    GlossarySealedTryGetValue |      .NET Core 3.1 | 2.528 us |  0.51 |
|                              |                    |          |       |
|        DictionaryTryGetValue | .NET Framework 4.8 | 6.965 us |  1.00 |
|          GlossaryTryGetValue | .NET Framework 4.8 | 2.575 us |  0.37 |
| GlossaryNonSealedTryGetValue | .NET Framework 4.8 | 2.546 us |  0.37 |
|    GlossarySealedTryGetValue | .NET Framework 4.8 | 2.528 us |  0.37 |

Соглашусь, слово "всегда" слишком сильное. В 80% случаев, кроме некоторых - вот тут относительно недавно обсуждалось.

Я хотел напомнить всё это лишь для того, чтобы объяснить почему я сказал относительно структур "runtime'у не надо будет выяснять по таблице виртуальных методов, кому принадлежит этот метод". Детально вдаваться в объяснения и пояснения, по которым люди пишут длинные посты и даже статьи я не намеревался.

Возможно, я выразился не очень точно. Я говорил об IL-коде, где вызов статического метода это call, вызов не статического метода - всегда callvirt(даже если метод не помечен ключевым словом virtual).

Вызов методов структур похож на статические методы, там тоже call, который несколько быстрее, чем callvirt.

Прочитать можно вот тут и тут.
Про call и callvirt можно тоже прочитать.

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

Однако, в данном конкретном случае я в версию инлайна верю слабо. Скорее тут это сделано для единообразия кода.

Спасибо большое! Как дойдут руки, я обязательно добавлю ваш код в статью.

Информация

В рейтинге
Не участвует
Откуда
Нижний Новгород, Нижегородская обл., Россия
Зарегистрирован
Активность

Специализация

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