Как читать бенчмарки #бенч
Давайте мы немного вспомним основы, и напомним, как читать результаты BenchmarkDotNet.
Итак, когда мы создали первый бенчмарк мы получаем примерно такую табличку с данными. Казалось бы, ну чего тут такого. Рассказываю.
1. В заголовке мы видим версию BenchmarkDotNet. Это важно, так как .NET меняется, а значит меняется и прибор, с помощью которого его измеряют.
2. Далее следует версия ОС. Это важно. Например, запуск на Windows и Linux может отличаться.
3. Далее идёт информация о процессоре. В данном случае она "Unknown processor", так как это контейнер. Однако, мы не можем сомневаться в том, что тип и разрядность процессора влияют на скорость работы.
4. Далее идёт версия .NET. Напомню, что разница производительности некоторых версий .NET поразительна. Иногда есть деградация, иногда - прорыв. На неё нужно обращать внимание.
5.
6.
7.
8.
9.
10.
Подробно о том, что есть что - пишется после каждого бенчмарка.
Давайте мы немного вспомним основы, и напомним, как читать результаты BenchmarkDotNet.
Итак, когда мы создали первый бенчмарк мы получаем примерно такую табличку с данными. Казалось бы, ну чего тут такого. Рассказываю.
1. В заголовке мы видим версию BenchmarkDotNet. Это важно, так как .NET меняется, а значит меняется и прибор, с помощью которого его измеряют.
2. Далее следует версия ОС. Это важно. Например, запуск на Windows и Linux может отличаться.
3. Далее идёт информация о процессоре. В данном случае она "Unknown processor", так как это контейнер. Однако, мы не можем сомневаться в том, что тип и разрядность процессора влияют на скорость работы.
4. Далее идёт версия .NET. Напомню, что разница производительности некоторых версий .NET поразительна. Иногда есть деградация, иногда - прорыв. На неё нужно обращать внимание.
5.
Method
- имя бенчмарка. Каждый бенчмарк запускается изолировано и сопровождается отдельным прогревом (подробности пока оставим). Мы можем быть уверенными в том, что бенчмарки не влияют друг на друга.6.
Mean
- это время выполнения бенчмарка. Замечу, что по-умолчанию, это усреднённое время выполнения 15 бенчмарков. Иногда - большего количества, если в процессе их выполнения были обнаружены статистические выбросы - тогда количество повторений увеличится вплоть до 100.7.
Ratio
- это отклонение скорости работы отдельных бенчмарков относительно основного (Baseline
). Его можно узнать по цифре "1.00". В данном случае это Storage
.8.
Gen0
, Gen1
, Gen2
- среднее количество сборок мусора по поколению на одно исполнение бенчмарка. Особенно важно, что эта статистика указывает, насколько наш GC (см. ОС и процессор) в нашем сценарии должен успевать собирать мусор.9.
Allocated
- общее количество аллоцированых данных в памяти. Помогает оценить верхнюю границу памяти, затраченной на один бенчмарк.10.
Alloc Ratio
- относительное количество затраченной памяти. Помогает оценивать работу алгоритма относительно Baseline
.Подробно о том, что есть что - пишется после каждого бенчмарка.
👍26
Разные платформы и процессоры #бенч
В продолжение разговора о разных ОС и процессорах, который был начат в посте про Random, необходимо понимать следующее.
И это нужно знать. Это нужно проверять. И с этим нужно смириться.
Ваш код на разных платформах и на разных процессорах будет исполняться по разному. Такова реальность. Особенно, если дело касается микрооптимизаций.
Вот, например, у нас простейший бенчмарк. Это старый бенчмарк, который я периодически запускаю на разных версиях .NET и на разных компьютерах. Бенчмарк пытается воспроизвести Array bound check elimination (оптимизацию JIT, которая позволяет избегать проверки границ массива). Штука уже не очень актуальная, но я люблю этот бенч, так как я подсмотрел его аж в 2015 году у некого Андрея.
И вот, мы снова запускаем его в 2024 году на одном .NET 8, но на разных машинах.
Что мы видим:
1. Версия BenchmakDotNet и самого .NET одинаковая.
2. Разные ОС (Windows 11 и MacOS 14.5).
3. Разные процессоры (Ryzen 5800H и M3 Max).
4. Разная скорость исполнения (46 ns против 29 ns).
5. И совершенно разные относительные результаты.
Если на Windows результат
IL код в моей IDE одинаковый.
Вывод: знайте результаты на целевой ОС и целевых процессорах. В идеале, нужно встроить проверку работы ключевых алгоритмов прямо в CI/CD. Всё иное (я говорю про микрооптимизации) - результат на вашей и только вашей машине.
Код бенчмарка в комментариях.
В продолжение разговора о разных ОС и процессорах, который был начат в посте про Random, необходимо понимать следующее.
И это нужно знать. Это нужно проверять. И с этим нужно смириться.
Ваш код на разных платформах и на разных процессорах будет исполняться по разному. Такова реальность. Особенно, если дело касается микрооптимизаций.
Вот, например, у нас простейший бенчмарк. Это старый бенчмарк, который я периодически запускаю на разных версиях .NET и на разных компьютерах. Бенчмарк пытается воспроизвести Array bound check elimination (оптимизацию JIT, которая позволяет избегать проверки границ массива). Штука уже не очень актуальная, но я люблю этот бенч, так как я подсмотрел его аж в 2015 году у некого Андрея.
И вот, мы снова запускаем его в 2024 году на одном .NET 8, но на разных машинах.
Что мы видим:
1. Версия BenchmakDotNet и самого .NET одинаковая.
2. Разные ОС (Windows 11 и MacOS 14.5).
3. Разные процессоры (Ryzen 5800H и M3 Max).
4. Разная скорость исполнения (46 ns против 29 ns).
5. И совершенно разные относительные результаты.
Если на Windows результат
_array.Length
и константы примерно одинаковый, то на MacOS результаты разные, что несколько внезапно и совершенно не понятно.IL код в моей IDE одинаковый.
Вывод: знайте результаты на целевой ОС и целевых процессорах. В идеале, нужно встроить проверку работы ключевых алгоритмов прямо в CI/CD. Всё иное (я говорю про микрооптимизации) - результат на вашей и только вашей машине.
Код бенчмарка в комментариях.
👍14🔥4❤3
Аутстафф #философия
Коллеги, у меня нет предубеждения перед аутстаффом. Ну, я про работу на галеры. Про компании, которые продают разработчиков другим компаниям. Я лишь хочу указать на некоторые нюансы для коллег с той и с другой стороны.
Итак, в чём преимущества быть аутстаффом:
1. Можно обучиться работать с разными технологиями и проектами. Быстро. Много. Это очень ценный опыт. Резюме будет прекрасным!
2. Рост от джуна до сеньора для серьезных мужиков и девчонок может быть стремительным. Если, конечно, слушать и запоминать. Проверено на собственном опыте.
3. Если проект или технологии не нравятся - их можно быстро сменить. Просто говоришь начальнику галеры, и он договаривается с покупателем. Ну или просит остаться за бонус. Всем хорошо.
4. Вроде как неплохо принимают джунов.
В чем плюсы аутстаффа для покупателя:
1. Экстренный ресурс. Например, быстро закрыть потребность в разработчиках на критических стадиях проекта.
2. Легко уволить. Надо просто не продлевать контракт. Никаких проблем с отработками и дополнительными выплатами - по законодательству аутстафф работает на другую компанию.
3. Чаще всего персонал как минимум уровня middle. Это значит, что коллеги умеют и будут копать. Наверное. Возможно. Если не подсунут джуна.
В чём минусы быть аутстаффом:
1. Могут не найти проект. В этом случае надо будет раскладывать пасьянс. Я серьёзно. Просто ходишь на работу и, например, пишешь какие-то внутренние документы, которые никто не читает. Или прям пасьянс.
2. Иногда ЗП зависит от прибыли с проекта. Например, ЗП это МРОТ, а всё остальное - бонус. В этом случае, получить нормальные деньги можно только тогда, когда есть проект. Иначе - пасьянс.
3. Мутные схемы работы. Например, иногда надо работать под акаунтом человека, который давно уволился. Но покупатель об этом не знает. И надо скрывать. И соответствовать тому чуваку. Помню, я уволился, а под моим акаунтом в одной американской компании ещё лет 5 сидели 10 разных людей.
4. Иногда надо работать на два проекта. Но каждый из покупателей об этом не знает. Например, вы работаете на проект и компанию А, а в Б внезапно надо быстро что-то сделать. Без погружения, без всяких сложностей. Дали задачу, дали доступ к репо - вперёд, есть ночь.
5. Иногда в резюме для покупателя пишут не то, что умеешь, а потом надо как-то притворяться тем, кем ты не являешься.
6. Увольнение стремительное и без объяснений.
7. В своё время в одной крупной американской компании, которая имела деятельность на территории РФ, были бейджики двух разных цветов. И снять его было нельзя. Надо ли говорить, что права разных бейджиков были разными?
В чем минусы аутстаффа для покупателя:
1. Иногда middle не нужен. А senior’ов нет, так как все они ушли в продуктовые компании. Поэтому будут подсовывать мидлов под видом сеньёров.
2. Обучение нового сотрудника не быстрое, а его могут просто не продлить на следующий месяц. И всё обучение «в трубу».
3. Врут в резюме. Или можно наткнуться на кандидата, который вообще не имеет опыта в нужных технологиях, но оплата была за каждого, кто был предоставлен, а поэтому гонят всех подряд. Без оценки. C++ и C#, ну какая, к бесу, разница?
4. Каждого кандидата надо собеседовать. Также, как и обычных. Иначе можно попасть в ситуацию, что подослали джуна с питоном, который работает на нескольких проектах.
5. Ад с доступами. Аутстафф не всегда человек по версии ИБ - нужна куча дополнительных согласований. И, кстати, не факт, что успешных.
6. Нужны хорошие процессы производства. Если у покупателя задачи в трекере описаны тезиснно, документации нет, тестов мало - будет большой проблемой объяснить аутстаффу, а что, собственно, нужно сделать.
Как это связано с производительностью? Если вы лид, то напрямую - вашу команду усиливают, а значит будут требовать результаты. Знайте плюсы и минусы. Ну, а если вы аутстафф - знайте то, в какой ситуации вам надо высекать каменный цветок.
P.S.: Привет моим коллегам с галеры! Это было крутое время!
Коллеги, у меня нет предубеждения перед аутстаффом. Ну, я про работу на галеры. Про компании, которые продают разработчиков другим компаниям. Я лишь хочу указать на некоторые нюансы для коллег с той и с другой стороны.
Итак, в чём преимущества быть аутстаффом:
1. Можно обучиться работать с разными технологиями и проектами. Быстро. Много. Это очень ценный опыт. Резюме будет прекрасным!
2. Рост от джуна до сеньора для серьезных мужиков и девчонок может быть стремительным. Если, конечно, слушать и запоминать. Проверено на собственном опыте.
3. Если проект или технологии не нравятся - их можно быстро сменить. Просто говоришь начальнику галеры, и он договаривается с покупателем. Ну или просит остаться за бонус. Всем хорошо.
4. Вроде как неплохо принимают джунов.
В чем плюсы аутстаффа для покупателя:
1. Экстренный ресурс. Например, быстро закрыть потребность в разработчиках на критических стадиях проекта.
2. Легко уволить. Надо просто не продлевать контракт. Никаких проблем с отработками и дополнительными выплатами - по законодательству аутстафф работает на другую компанию.
3. Чаще всего персонал как минимум уровня middle. Это значит, что коллеги умеют и будут копать. Наверное. Возможно. Если не подсунут джуна.
В чём минусы быть аутстаффом:
1. Могут не найти проект. В этом случае надо будет раскладывать пасьянс. Я серьёзно. Просто ходишь на работу и, например, пишешь какие-то внутренние документы, которые никто не читает. Или прям пасьянс.
2. Иногда ЗП зависит от прибыли с проекта. Например, ЗП это МРОТ, а всё остальное - бонус. В этом случае, получить нормальные деньги можно только тогда, когда есть проект. Иначе - пасьянс.
3. Мутные схемы работы. Например, иногда надо работать под акаунтом человека, который давно уволился. Но покупатель об этом не знает. И надо скрывать. И соответствовать тому чуваку. Помню, я уволился, а под моим акаунтом в одной американской компании ещё лет 5 сидели 10 разных людей.
4. Иногда надо работать на два проекта. Но каждый из покупателей об этом не знает. Например, вы работаете на проект и компанию А, а в Б внезапно надо быстро что-то сделать. Без погружения, без всяких сложностей. Дали задачу, дали доступ к репо - вперёд, есть ночь.
5. Иногда в резюме для покупателя пишут не то, что умеешь, а потом надо как-то притворяться тем, кем ты не являешься.
6. Увольнение стремительное и без объяснений.
7. В своё время в одной крупной американской компании, которая имела деятельность на территории РФ, были бейджики двух разных цветов. И снять его было нельзя. Надо ли говорить, что права разных бейджиков были разными?
В чем минусы аутстаффа для покупателя:
1. Иногда middle не нужен. А senior’ов нет, так как все они ушли в продуктовые компании. Поэтому будут подсовывать мидлов под видом сеньёров.
2. Обучение нового сотрудника не быстрое, а его могут просто не продлить на следующий месяц. И всё обучение «в трубу».
3. Врут в резюме. Или можно наткнуться на кандидата, который вообще не имеет опыта в нужных технологиях, но оплата была за каждого, кто был предоставлен, а поэтому гонят всех подряд. Без оценки. C++ и C#, ну какая, к бесу, разница?
4. Каждого кандидата надо собеседовать. Также, как и обычных. Иначе можно попасть в ситуацию, что подослали джуна с питоном, который работает на нескольких проектах.
5. Ад с доступами. Аутстафф не всегда человек по версии ИБ - нужна куча дополнительных согласований. И, кстати, не факт, что успешных.
6. Нужны хорошие процессы производства. Если у покупателя задачи в трекере описаны тезиснно, документации нет, тестов мало - будет большой проблемой объяснить аутстаффу, а что, собственно, нужно сделать.
Как это связано с производительностью? Если вы лид, то напрямую - вашу команду усиливают, а значит будут требовать результаты. Знайте плюсы и минусы. Ну, а если вы аутстафф - знайте то, в какой ситуации вам надо высекать каменный цветок.
P.S.: Привет моим коллегам с галеры! Это было крутое время!
👍26❤2🥱2
Микросервисы vs монолит #доклад
Что-то я пропустил доклад некого Станислава о монолитах, через микросервисы и обратно, но уже в модули. Всем, кого данная тема волнует, я рекомендую это видео.
1. Глубоко, как это принято у Станислава, затронута история вопроса и предпосылки, которые толкают нас от монолитов к микросервисам.
2. Отмечен чисто эмпирический эффект, когда разработчики (или требуют заказчики) закладывают ресурсы на каждый микросервис без понимания границы ресурсов кластера при наличии горизонтального размножения сервисов.
3. Рассказана байка о "черной пятнице", когда взрывной рост нагрузки вызывает каскадный эффект на сотне микросервисов, которые пытаются удвоить потребление ресурсов.
4. Также, подробно, как Станислав любит, рассказана история слияния микросервисов обратно в монолит.
5. Продемонстрирована предварительная статистика слияния 11 сервисов в один, что привело к снижению потребления ОЗУ в 5 раз, а процессора - в 2 раза.
6. Затронут вопрос о том, что взаимодействие сервисов тоже стоит процессорного времени и ОЗУ. И это ещё мы забываем про то, что наличие микросервисов ест ресурсы внешних систем - очередей, балансеров, различных демонов и прочее-прочее.
7. Модульная архитектура позволяет отложить вопрос о том, нужен нам модульный монолит или всё-таки микросервисы, так как вы можете принять решение в моменте.
Код Станислав выложил вот сюда.
Также, я настоятельно рекомендую раскрутить некого Руслана ещё раз рассказать, но уже на камеру, вот этот доклад про путь от микросервисов к модулям.
Что-то я пропустил доклад некого Станислава о монолитах, через микросервисы и обратно, но уже в модули. Всем, кого данная тема волнует, я рекомендую это видео.
1. Глубоко, как это принято у Станислава, затронута история вопроса и предпосылки, которые толкают нас от монолитов к микросервисам.
2. Отмечен чисто эмпирический эффект, когда разработчики (или требуют заказчики) закладывают ресурсы на каждый микросервис без понимания границы ресурсов кластера при наличии горизонтального размножения сервисов.
3. Рассказана байка о "черной пятнице", когда взрывной рост нагрузки вызывает каскадный эффект на сотне микросервисов, которые пытаются удвоить потребление ресурсов.
4. Также, подробно, как Станислав любит, рассказана история слияния микросервисов обратно в монолит.
5. Продемонстрирована предварительная статистика слияния 11 сервисов в один, что привело к снижению потребления ОЗУ в 5 раз, а процессора - в 2 раза.
6. Затронут вопрос о том, что взаимодействие сервисов тоже стоит процессорного времени и ОЗУ. И это ещё мы забываем про то, что наличие микросервисов ест ресурсы внешних систем - очередей, балансеров, различных демонов и прочее-прочее.
7. Модульная архитектура позволяет отложить вопрос о том, нужен нам модульный монолит или всё-таки микросервисы, так как вы можете принять решение в моменте.
Код Станислав выложил вот сюда.
Также, я настоятельно рекомендую раскрутить некого Руслана ещё раз рассказать, но уже на камеру, вот этот доклад про путь от микросервисов к модулям.
YouTube
Станислав Сидристый — Гибридная архитектура: слияние микросервисов в монолит по необходимости
Подробнее о конференции DotNext: https://jrg.su/3WmFRE
— —
При необходимости работать в различных окружениях — и на дистанции в несколько сотен серверов, и на одном сервере на вообще все сервисы — возникает целый ряд проблем, совершенно неспецифичных в обычной…
— —
При необходимости работать в различных окружениях — и на дистанции в несколько сотен серверов, и на одном сервере на вообще все сервисы — возникает целый ряд проблем, совершенно неспецифичных в обычной…
👍24❤6
ObjectPool #память #решение
Есть такая работа - ObjectPool. Это когда мы не позволяем GC уничтожать объекты, которые живут очень короткое время, а используем их снова и снова. Это очень помогает в экономии памяти.
Если кто не знал, то
Существует хорошая библиотека, которая позволяет это делать. Причём не от кого-нибудь, а от вендора. Это быстрая и хорошо написанное решение.
Но что делать, если нам нужно быстрее?
Ответ, как всегда, есть.
1. Велосипед. Это
2. Можно воспользоваться той самой "другой" библиотекой. Скорость выше не на порядки, но, тем не менее, выше. А хорошее и подробное описание кода может погрузить нас в прекрасный мир высокой производительности.
В результате, мы получим либо аналогичную производительность (но без зависимостей), либо производительность чуть-чуть выше, чем у того, что предлагает нам вендор.
При этом, внимательный читатель наверняка заметил, что самым эффективным пулом для маленьких объектов является некий
Если кому-то мало комментариев Евгения и хочется большего, то могу сообщить дополнительно, что имплементация ConcurrentQueue в .NET выросла вот отсюда. Она же, чаще всего, лежит в основах примеров в интернете.
Кода много, поэтому он тут.
Есть такая работа - ObjectPool. Это когда мы не позволяем GC уничтожать объекты, которые живут очень короткое время, а используем их снова и снова. Это очень помогает в экономии памяти.
Если кто не знал, то
new
- дорогая операция (хотя есть мнение, что нет), которая требует выделить памяти в heap (для классов, конечно же). Ну в куче, которую контролирует GC. Чтобы не заставлять GC работать (а его работа это дорогой обход дерева), мы можем помещать объекты, которые живут не долго, в специальное место - пул. Извлекая их оттуда, мы их переиспользуем, то есть не заставляем их снова и снова появляться в куче.Существует хорошая библиотека, которая позволяет это делать. Причём не от кого-нибудь, а от вендора. Это быстрая и хорошо написанное решение.
Но что делать, если нам нужно быстрее?
Ответ, как всегда, есть.
1. Велосипед. Это
Manual
в бенчмарке. В принципе, там нет ничего особенного - я списал какие-то, с моей точки зрения, важные вещи с реализации Microsoft и из другой билиблиотеки. Это решение лаконично и понятно.2. Можно воспользоваться той самой "другой" библиотекой. Скорость выше не на порядки, но, тем не менее, выше. А хорошее и подробное описание кода может погрузить нас в прекрасный мир высокой производительности.
В результате, мы получим либо аналогичную производительность (но без зависимостей), либо производительность чуть-чуть выше, чем у того, что предлагает нам вендор.
При этом, внимательный читатель наверняка заметил, что самым эффективным пулом для маленьких объектов является некий
ConcurrentToolkitLite
. Это внутренний класс библиотеки ConcurrentToolkit. Его реализация проста, и основана на том, что существует всего один ThreadStatic объект, который и содержит данные пула. Вот так просто и элегантно.Если кому-то мало комментариев Евгения и хочется большего, то могу сообщить дополнительно, что имплементация ConcurrentQueue в .NET выросла вот отсюда. Она же, чаще всего, лежит в основах примеров в интернете.
Кода много, поэтому он тут.
👍12🔥5❤4
ContinueWith #скорость
Не секрет, что мы можем использовать метод ContinueWith для небольшого увеличения производительности. Давно об этом знал, но всё руки не доходили протестировать. Так вот, докладываю.
Делается это просто - мы можем вызвать наш асинхронный метод, а затем, не используя
Это будет несколько быстрее, чем:
Отлично применяется с известной многим сущностью
Напомню, что минусом применения подхода с
P.S.: Бенчмарк в комментариях.
P.P.S: Алексею и Игорю спасибо) Было весело это всё отлаживать.
Не секрет, что мы можем использовать метод ContinueWith для небольшого увеличения производительности. Давно об этом знал, но всё руки не доходили протестировать. Так вот, докладываю.
Делается это просто - мы можем вызвать наш асинхронный метод, а затем, не используя
await
, написать что-то вроде:
MyAsyncMethod(cancellation).ContinueWith(task => DoSomething(task.Result), cancellation);
Это будет несколько быстрее, чем:
var result = await MyAsyncMethod(cancellation);
DoSomething(result);
Отлично применяется с известной многим сущностью
Result<T>
, где, в зависимости от этого результата нужно что-то сделать или не сделать.Напомню, что минусом применения подхода с
ContinueWith
является то, что логи с ошибками становятся немного... плохо читаемыми.P.S.: Бенчмарк в комментариях.
P.P.S: Алексею и Игорю спасибо) Было весело это всё отлаживать.
🔥16👍7❤1
Логика на throw #скорость #память
Известно, что логика на
Типа, всем известно, что выброс ошибки, её перехват и раскручивание стека вызова - дорогая операция. Но меня давно интересовало, а, собственно, насколько "дорого" строить логику на throw? Как раз недавно, на собеседовании, был затронут этот вопрос.
Итак, докладываю. Бенчмарк будет в комментариях.
1. Обычный
2. Выброс ошибки не только в восемь тыщ (!) раз медленнее, но и аллоцирует. Немного, в Gen0, но очень неприятно в горячих местах кода.
3. Если возвращать ошибку в Result (очень популярная фишка из функциональщины), то это чуть-чуть медленнее обычного
Выводы: не надо строить логику на ошибках (а кто бы сомневался), ну а если нам очень надо всё-таки возвращать ошибку коду выше, но без
Казалось бы, очевидно. Но нет, иногда таки встречается в реальном коде.
P.S.: Сергей, спасибо за вопрос.
P.P.S.: Коллега напоминает, что про дорогой выброс ошибки ещё писали вот тут.
Известно, что логика на
throw
- не очень. Ну, это когда мы выбрасываем ошибку в методе, окружаем его вызов try/catch и, в зависимости от того, была ли ошибка, выбираем тот или иной сценарий выполнения.Типа, всем известно, что выброс ошибки, её перехват и раскручивание стека вызова - дорогая операция. Но меня давно интересовало, а, собственно, насколько "дорого" строить логику на throw? Как раз недавно, на собеседовании, был затронут этот вопрос.
Итак, докладываю. Бенчмарк будет в комментариях.
1. Обычный
if/else
вне конкуренции.2. Выброс ошибки не только в восемь тыщ (!) раз медленнее, но и аллоцирует. Немного, в Gen0, но очень неприятно в горячих местах кода.
3. Если возвращать ошибку в Result (очень популярная фишка из функциональщины), то это чуть-чуть медленнее обычного
if
.Выводы: не надо строить логику на ошибках (а кто бы сомневался), ну а если нам очень надо всё-таки возвращать ошибку коду выше, но без
throw
, то делаем это с помощью Result
. Казалось бы, очевидно. Но нет, иногда таки встречается в реальном коде.
P.S.: Сергей, спасибо за вопрос.
P.P.S.: Коллега напоминает, что про дорогой выброс ошибки ещё писали вот тут.
👍33🔥6❤1
Танцы вокруг Enumerable.ToArray #скорость #память
Если мы пишем код по гайдлайнам, то наши методы часто возвращают
При получении
В этой ситуации некоторые программисты допускают первую логическую ошибку. Они подсматривают код метода, который возвращает интерфейс коллекции, видят, что за ним на самом деле скрывается массив, и делают предположение о том, что им тоже нужно использовать исходную коллекцию, скрытую за интерфейсом.
Далее действия могут быть различными и зависеть от знаний глубин .NET'a.
Например, некоторые коллеги верят, что если они вызовут метод
Другие коллеги будут более упорными в желании добраться до исходной коллекции, и создадут метод
Скорость сильно выше, никаких дополнительных аллокаций сделано не будет (см. бенчмарк), а значит будет сделан вывод, что это идеальное решение для ситуаций работы с приходящим
Казалось бы win-win. Но нет. И это вторая логическая ошибка.
Если мы ожидаем массив, исходный метод делает массив и мы придумали костыль, чтоб всё-таки получить массив... не лучше ли просто возвращать массив? Зачем эти танцы с бубном?
Если это наш код и нам ну очень нужен конкретный тип коллекции - давайте просто его использовать. Да, если до этого был IEnumerable, а теперь стал массив - это ломающее изменение и оно не подойдёт для библиотеки с тыщами потребителей... но уж внутри нашего приложения медиаторный хэндлер мы поправить в состоянии.
Ещё одним неприятным моментом будет изменение исходной коллекции. Например, мы получили перечисление, скастили его к массиву и изменили... А оно, например, служит неким кэшем. Как результат, возможно очень странное поведение в других местах кода, которые полагаются на неизменяемость исходной коллекции.
Короче говоря, мне кажется, что иногда не нужно изобретать велосипед. В данном случае, я в этом почти уверен.
P.S.: Александр, спасибо!
P.P.S.: Для серьёзных ребят Денис сделал собственный перечислитель для разных случаев. Кажется, что это очень хорошее решение, которое ликвидирует минусы подобного подхода.
Если мы пишем код по гайдлайнам, то наши методы часто возвращают
IEnumerable<T>
, IReadonlyCollection<T>
и прочие интерфейсы коллекций. Это необходимо для того, чтобы сигнатура метода не изменялась при изменении логики метода, что, в свою очередь, является фундаментом для создания устойчивого к изменениям кода. В принципе, очень полезная и весьма здравая мысль.При получении
IEnumerable
из какого-либо метода, мы часто делаем ToList
или ToArray
. Например, чтобы получить возможность пробежаться по перечислению более чем один раз (см. multiple enumerations в случае IEnumerable). Или, например, мы не хотим аллоцировать Enumerator
при пробегании по IReadonlyCollection
. Короче говоря, по каким-то перформансным соображениям, нам интерфейсы не подходят.В этой ситуации некоторые программисты допускают первую логическую ошибку. Они подсматривают код метода, который возвращает интерфейс коллекции, видят, что за ним на самом деле скрывается массив, и делают предположение о том, что им тоже нужно использовать исходную коллекцию, скрытую за интерфейсом.
Далее действия могут быть различными и зависеть от знаний глубин .NET'a.
Например, некоторые коллеги верят, что если они вызовут метод
ToArray
, то произойдёт магия: мол, dotnet знает настоящий тип коллекции, которая возвращается из метода, а значит просто его и вернёт. Увы, это не так. Если мы посмотрим код, то можно заметить, что при вызове Enumerable.ToArray
создаётся новый массив с копией данных исходного, который и возвращается потребителю.Другие коллеги будут более упорными в желании добраться до исходной коллекции, и создадут метод
AsArray
. Он, я уверен, есть во многих проектах. Этот метод прост, он проверяет тип, и, если это действительно массив, просто возвращает его. Если же это другой тип коллекции, то будет использоваться стандартный Enumerable.ToArray
.Скорость сильно выше, никаких дополнительных аллокаций сделано не будет (см. бенчмарк), а значит будет сделан вывод, что это идеальное решение для ситуаций работы с приходящим
IEnumerable
. Код, думаю, будет примерно таким:
public static T[] AsArray<T>(this IEnumerable<T> collection)
{
return collection as T[] ?? collection.ToArray();
}
Казалось бы win-win. Но нет. И это вторая логическая ошибка.
Если мы ожидаем массив, исходный метод делает массив и мы придумали костыль, чтоб всё-таки получить массив... не лучше ли просто возвращать массив? Зачем эти танцы с бубном?
Если это наш код и нам ну очень нужен конкретный тип коллекции - давайте просто его использовать. Да, если до этого был IEnumerable, а теперь стал массив - это ломающее изменение и оно не подойдёт для библиотеки с тыщами потребителей... но уж внутри нашего приложения медиаторный хэндлер мы поправить в состоянии.
Ещё одним неприятным моментом будет изменение исходной коллекции. Например, мы получили перечисление, скастили его к массиву и изменили... А оно, например, служит неким кэшем. Как результат, возможно очень странное поведение в других местах кода, которые полагаются на неизменяемость исходной коллекции.
Короче говоря, мне кажется, что иногда не нужно изобретать велосипед. В данном случае, я в этом почти уверен.
P.S.: Александр, спасибо!
P.P.S.: Для серьёзных ребят Денис сделал собственный перечислитель для разных случаев. Кажется, что это очень хорошее решение, которое ликвидирует минусы подобного подхода.
👍15👎10❤7
Боксинг IEnumerator #скорость #память #бенч
Кажется, что не всем понятна борьба за использование исходной коллекции в .NET. Поясняю весьма избитую истину - бежать по
Позволю себе напомнить внутреннюю работу .NET на примере List<T>.
Класс
Но стоит нам скастить List<T> к IList<T> - ситуация изменится. В этом случае foreach вызовет метод интерфейса
Насколько всё это будет медленнее - см. результаты бенчмарка.
Кажется, что не всем понятна борьба за использование исходной коллекции в .NET. Поясняю весьма избитую истину - бежать по
IEnumerable
(IList, IReadOnlyCollection и т.п.) сильно дороже, чем по исходной коллекции. Как по памяти, так и по скорости. Позволю себе напомнить внутреннюю работу .NET на примере List<T>.
Класс
List<T>
обладает методом GetEnumerator
, который вызывается при попытке сделать по нему foreach
. Как можно заметить, List<T>.Enumerator
- структура. Это важно, поскольку структура создаётся на стеке, а её методы вызываются напрямую (см. call и callvirt). Это позволяет пробегать по списку быстро и без затрат на выделение места в куче, а значит без работы GC.Но стоит нам скастить List<T> к IList<T> - ситуация изменится. В этом случае foreach вызовет метод интерфейса
IEnumerable.GetEnumerator
, который возвращает IEnumerator<T>
, то есть интерфейс перечислителя. В случае List’a это будет та же самая структура, но размещённая в куче и доступом к её методам через callvirt. То есть мы не только создадим небольшой memory traffic, но и сильно замедлим перебор коллекции.Насколько всё это будет медленнее - см. результаты бенчмарка.
👍29❤6🔥1
Кажется, надо попробовать! Мистеру успехов в развитии канала.
Я также, как и Евгений, достаточно скептично отношусь к способности современных "чатов" делать что-то полезное для программиста. Помню давным-давно пробовал, но меня не впечатлило. Кажется, было много ошибок, а я получал какой-то код уровня джуна с курсов "Шарп за неделю".
Возможно, сейчас настала пора попробовать снова. Тем более и циферки появились (бенчмарки), и инфраструктуру какую-никакую для IDE уже создали, да и сама технология более-менее избавилась от первых проблем.
https://t.iss.one/probelov_net/21
Я также, как и Евгений, достаточно скептично отношусь к способности современных "чатов" делать что-то полезное для программиста. Помню давным-давно пробовал, но меня не впечатлило. Кажется, было много ошибок, а я получал какой-то код уровня джуна с курсов "Шарп за неделю".
Возможно, сейчас настала пора попробовать снова. Тем более и циферки появились (бенчмарки), и инфраструктуру какую-никакую для IDE уже создали, да и сама технология более-менее избавилась от первых проблем.
https://t.iss.one/probelov_net/21
Telegram
Пробелов.NET – AI в программировании и .NET
Лучшая модель для кодирования - Claude Sonnet 3.5
Вы уже наверняка слышали, что Anthropic на днях выпустили новую LLM, которая во многих бенчмарках обходит gpt-4o. Таким образом, Claude Sonnet 3.5 становится лучшей моделей для написания кода на сегодня.…
Вы уже наверняка слышали, что Anthropic на днях выпустили новую LLM, которая во многих бенчмарках обходит gpt-4o. Таким образом, Claude Sonnet 3.5 становится лучшей моделей для написания кода на сегодня.…
🔥6🥰1
TryGetNonEnumerated #память
Хотелось бы напомнить про такую банальную, но весьма полезную оптимизацию, как создание списка с заранее известным размером.
Напомню, что первоначально
Однако, увы, некоторые методы возвращают
Это точно не
В нашем случае он может быть применён вот так:
Почему этот подход не используется в конструкторе того-же List’a (который принимает IEnumerable) и в его же методе
P.S.: Для желающих посмотреть, что метод TryGetNonEnumerated действительно ничего не перебирает, а просто возвращает значение - в комментариях есть бенчмарк.
Хотелось бы напомнить про такую банальную, но весьма полезную оптимизацию, как создание списка с заранее известным размером.
Напомню, что первоначально
List<T>
создаётся с внутренним массивом размера 0. При последующем добавлении элементов происходит проверка, и, если места не хватает, внутренний массив расширяется на свой размер, умноженный на 2. «Расширение», в данном случае, означает, что создаётся новый массив, а содержимое старого массива копируется в новый. Таким образом, если мы заранее создадим List с внутренним массивом в 200 элементов, то мы избежим аллокаций шести массивов - это весьма солидно .Однако, увы, некоторые методы возвращают
IEnumerable<T>
, из которого весьма проблематично узнать размер. Да, за IEnumerable<T> может скрываться любая из коллекций, имплементирующая ICollection<T>
, и тогда проблем с выяснением первоначального размера нашей коллекции нет. Но что если нам пришёл результат чего-то вот такого?
_data = array // первоначальный массив
.Skip(1)
.Take(Count / 2)
.Order()
.Select(static i => i * i % 10 == 0 ? "да" : "нет");
Это точно не
ICollection<string>
, то есть выяснить размер мы не сможем. Вернее, всё-таки сможем. Если заглянуть в недра .NET, то мы узнаем, что это некий Enumerable.SelectIPartitionIterator
, который, на наше счастье, реализует внутренний интерфейс IIListProvider
. Чтобы попытаться воспользоваться его методом GetCount
, нам поможет метод TryGetNonEnumeratedCount, который появился аж в .NET 6.В нашем случае он может быть применён вот так:
var capacity = _data.TryGetNonEnumeratedCount(out var count)
? count
: ваша_эвристическая_константа;
var list = new List<string>(capacity);
list.AddRange(_data);
Почему этот подход не используется в конструкторе того-же List’a (который принимает IEnumerable) и в его же методе
AddRange
- загадка. Наверное, у коллег пока просто не дошли руки.P.S.: Для желающих посмотреть, что метод TryGetNonEnumerated действительно ничего не перебирает, а просто возвращает значение - в комментариях есть бенчмарк.
👍24❤5
Работа с ArrayPool и MemoryPool #память
Пул массивов - классная штука, но есть проблема. И даже две.
Первая проблема. Когда мы пытаемся получить
Соответственно, при попытке итерироваться по полученному массиву, мы можем столкнуться с пустыми элементами, либо элементами, которые содержат предыдущие данные (очистка массива перед возвратом в пул это право, а не обязанность потребителя).
Вариантов решения этой проблемы несколько. Во-первых, мы можем воспользоваться
Теперь мы можем передавать ArraySegment в нужные методы, и избавить потребителей от необходимости знать о том, какого же размера массив был запрошен и необходим.
Во-вторых, мы можем воспользоваться
В-третьих, можно использовать
Memory это обычная структура, а значит нет никаких проблем передавать её в
Вторая проблема при работе с ArrayPool - возврат использованного массива. Когда мы закончили работу с массивом из пула, логично было бы вернуть его в пул в том же методе, в котором мы его получили. Более того, мне кажется, что это правильное решение с точки зрения архитектуры.
Если мы обратим внимание на использование MemoryPool, то он возвращает не Memory, а
Замечу ещё раз, этот способ не совсем правильный и является выходом из ситуации, когда нам всё-таки не удобно возвращать массив в пул по месту получения.
Другие способы (ArraySegment или Span) такой конструкцией не обладают, и мы должны самостоятельно придумывать костыли для возврата массива в пул. Например, можно написать микс ArraySegment’a с IMemoryOwner, который обладает возможностью возврата массива в пул при вызове Dispose. Его код будет в комментариях. А можно ещё и вот так.
Используя подобный велосипед, вы сможете передавать в методы-потребители структуру
Пул массивов - классная штука, но есть проблема. И даже две.
Первая проблема. Когда мы пытаемся получить
ArrayPool<T>.Shared.Rent
массив размером, допустим, в 4 элемента, мы получаем массив размером в 16 элементов. Если запросим 16, то получим 16, а вот если нам нужно 17 элементов, то мы получим аж 32. Таким образом, при запросе массива, мы всегда получаем массив размером не менее нужного. Это сделано специально, чтобы не аллоцировать большое количество массивов разного размера. Соответственно, при попытке итерироваться по полученному массиву, мы можем столкнуться с пустыми элементами, либо элементами, которые содержат предыдущие данные (очистка массива перед возвратом в пул это право, а не обязанность потребителя).
Вариантов решения этой проблемы несколько. Во-первых, мы можем воспользоваться
ArraySegment
- это структура, которая знает размер необходимого нам сегмента массива и следит за тем, чтобы мы не вышли за его пределы при итерировании.var pool = ArrayPool<int>.Shared;
var array = pool.Rent(length);
var segment = new ArraySegment<int>(array, 0, length);
Теперь мы можем передавать ArraySegment в нужные методы, и избавить потребителей от необходимости знать о том, какого же размера массив был запрошен и необходим.
Во-вторых, мы можем воспользоваться
Span
. Это отличный вариант, когда мы передаём кусочек массива в методы, которые не являются async
. Напомню, нам запрещено работать с ref struct
в асинхронных методах.…
var span = array.AsSpan(..length);
В-третьих, можно использовать
Memory
- структурой, которая так же как и ArraySegment, указывает на область памяти. Конечно, при данном подходе, логичнее использовать не ArrayPool, а MemoryPool, который работает примерно так же, как и ArrayPool.var pool = MemoryPool<int>.Shared;
using var memoryOwner = pool.Rent(length);
var memory = memoryOwner.Memory[..length];
Memory это обычная структура, а значит нет никаких проблем передавать её в
async
методы и не думать в них о том, какой же реальный размер памяти мы используем. Напомню, что взаимодействие с Memory осуществляется через Span (memory.Span
).Вторая проблема при работе с ArrayPool - возврат использованного массива. Когда мы закончили работу с массивом из пула, логично было бы вернуть его в пул в том же методе, в котором мы его получили. Более того, мне кажется, что это правильное решение с точки зрения архитектуры.
var pool = ArrayPool<int>.Shared;
var array = pool.Rent(length);
DoSomething(array.AsSpan(..length));
pool.Return(array);
Если мы обратим внимание на использование MemoryPool, то он возвращает не Memory, а
IMemoryOwner
, что как бы намекает: есть владелец памяти, а есть методы, которые память используют. Передавая в методы использования и Memory, и IMemoryOwner’a, мы делаем утверждение, что теперь другой метод является владельцем области памяти, а значит именно он ответственен за её очистку. var pool = MemoryPool<int>.Shared;
var memoryOwner = pool.Rent(length);
var memory = memoryOwner.Memory[..length];
DoSomething(memory, memoryOwner);
Замечу ещё раз, этот способ не совсем правильный и является выходом из ситуации, когда нам всё-таки не удобно возвращать массив в пул по месту получения.
Другие способы (ArraySegment или Span) такой конструкцией не обладают, и мы должны самостоятельно придумывать костыли для возврата массива в пул. Например, можно написать микс ArraySegment’a с IMemoryOwner, который обладает возможностью возврата массива в пул при вызове Dispose. Его код будет в комментариях. А можно ещё и вот так.
Используя подобный велосипед, вы сможете передавать в методы-потребители структуру
PooledArray<T>
и в нужном месте вызывать Dispose, который вернёт массив в пул. В принципе, весьма неплохое решение. Если смотреть на бенчмарк всего сценария, то получается даже быстро.👍19❤3
Быстро парсим float #скорость #решение
Нашёл тут для себя задачку по парсингу большого количества значений из большого файла. В файле перечислены города и температуры в них. В принципе, ничего особенного -
Этот набор байтиков мы, конечно, как знающие люди, можем запихнуть в эффективный
И вот я, значит, написал этот высокоэффективный код, смотрю с помощью dotTrace на предмет, а что, собственно, можно ещё улучшить. Если кто не знает, эта такая утилитка, которая показывает, где и на что мы тратим время в коде. И, к моему большому удивлению оказалось, что куча времени тратится на
Я как-то даже немного подрасстроился. Ну, думаю, уж эту штуку должны были написать круто. Может быть я как-то не так готовлю
Я человек прошареный, поэтому искал решения в том числе на C++. Оказалось, что есть такая интересная штука как fast_float. И, о чудо, эта имплементация нашлась и на C#. Бенчмарк, замеры… Да, оказалось сильно быстрее, аж в пять раз. Человеку, который это придумал - респект. Код будет в комментариях.
Библиотеку слишком плотно не тестировал, но в деле решения ускорения исходной задачи она сильно помогает. Возможно, кто-то знает какие-то более классные решения - буду рад посмотреть.
P.S.: Сергей, спасибо за задачку!
P.P.S: Сравнение с
Нашёл тут для себя задачку по парсингу большого количества значений из большого файла. В файле перечислены города и температуры в них. В принципе, ничего особенного -
название_города;температура
. Температура это число с плавающей запятой (например, -2.33, 7.4, 0.3). В принципе, понятно, что если извлечь строку и поделить её по символу точка-с-запятой, то мы получим набор байтиков с числом.Этот набор байтиков мы, конечно, как знающие люди, можем запихнуть в эффективный
float.Parse
. Код получается примерно вот такой:
ReadOnlySpan<byte> row = …;
int separator = row.IndexOf(‘;’); // байтик
ReadOnlySpan<byte> temperatureBytes = row[(separator + 1)..];
float temperature = float.Parse(temperatureBytes, Culture);
И вот я, значит, написал этот высокоэффективный код, смотрю с помощью dotTrace на предмет, а что, собственно, можно ещё улучшить. Если кто не знает, эта такая утилитка, которая показывает, где и на что мы тратим время в коде. И, к моему большому удивлению оказалось, что куча времени тратится на
float.Parse
. На 100 миллионах строк это видно отчётливо…Я как-то даже немного подрасстроился. Ну, думаю, уж эту штуку должны были написать круто. Может быть я как-то не так готовлю
float.Parse
? Или что-то не докрутил с настройками (там они есть, их несколько)? Масса вопросов. Взял себя в руки, пошёл искать в интернетах, мол, что люди делают в такой ситуации. Я человек прошареный, поэтому искал решения в том числе на C++. Оказалось, что есть такая интересная штука как fast_float. И, о чудо, эта имплементация нашлась и на C#. Бенчмарк, замеры… Да, оказалось сильно быстрее, аж в пять раз. Человеку, который это придумал - респект. Код будет в комментариях.
Библиотеку слишком плотно не тестировал, но в деле решения ускорения исходной задачи она сильно помогает. Возможно, кто-то знает какие-то более классные решения - буду рад посмотреть.
P.S.: Сергей, спасибо за задачку!
P.P.S: Сравнение с
Utf8Parser.TryParse
(который, вроде как, реализует fast_float) тут. Евгений, спасибо!🔥20👍3😁1
Словари: поиск по ключу #бенч
Хотелось бы напомнить про скорость извлечения данных из словарей в .NET. Напомню, что словарей у нас много и все они немного разные -
Для того, чтобы грамотно выбрать словарь, я рекомендую послушать один из последних выпусков RadioDotNet (00:54:30). Там неплохо рассказали о том, как появились имплементации словарей, как они работают, где их слабые и сильные стороны.
При чтении бенчмарка прошу обратить внимание на то, что я использую интерфейс
Обратите внимание, что этот бенчмарк касается только извлечения данных по ключу. При выборе конкретной имплементацию этого бенчмарка не достаточно, и нужно обязательно обратить внимание на тип ключа и ваш профиль нагрузки - часто добавление значения в коллекцию полностью нивелирует скорость извлечения данных.
P.S.: Также, коллеги напоминают, что производительность FrozenDictionary очень зависит от ключа.
P.P.S: Сравнение скорости работы, когда ключ это строка тут. Там возможности FrozenDictionary раскрываются во всей красе.
Хотелось бы напомнить про скорость извлечения данных из словарей в .NET. Напомню, что словарей у нас много и все они немного разные -
Dictionary
, ConcurrentDictionary
, FrozenDictionary
, ImmutableDictionary
и ReadonlyDictionary
. У всех у них чуть-чуть разные задачи и немного разная производительность на разных ключах (в том числе на содержимом ключей). Для того, чтобы грамотно выбрать словарь, я рекомендую послушать один из последних выпусков RadioDotNet (00:54:30). Там неплохо рассказали о том, как появились имплементации словарей, как они работают, где их слабые и сильные стороны.
При чтении бенчмарка прошу обратить внимание на то, что я использую интерфейс
IDictionary
, а не явные имплементации словарей. Мотивация создания именно такого бенчмарка - поиск пути безопасной подмены имплементации одного словаря на другой в уже работающей кодовой базе. Естественно, при написании нового кода, я бы предпочёл использовать явные имплементаци (т.е. классы, а не интерфейс). Более того, IDE явно на это намекает.
CA1859: используйте конкретные типы, если это возможно для повышения производительности.
Обратите внимание, что этот бенчмарк касается только извлечения данных по ключу. При выборе конкретной имплементацию этого бенчмарка не достаточно, и нужно обязательно обратить внимание на тип ключа и ваш профиль нагрузки - часто добавление значения в коллекцию полностью нивелирует скорость извлечения данных.
P.S.: Также, коллеги напоминают, что производительность FrozenDictionary очень зависит от ключа.
P.P.S: Сравнение скорости работы, когда ключ это строка тут. Там возможности FrozenDictionary раскрываются во всей красе.
👍14🔥4❤1
Бесплатные Rider и WebStorm
Ого, решительно поддерживаю! Обещают, что приложение будет полностью функциональным и ни чем не отличаться от платной версии. Хорошее решение для пет-проектов и не только.
Злые языки утверждают, что такой шаг со стороны чехов - последствия снижения качества, низких продаж и жаркого дыхания VS и VSCode в спину Rider'у. Но я желаю парням успехов.
Ещё интернетах пишут, что эти IDE уже можно скачать через некий uTorrent. Не знаю, что это за приложение - надо попробовать. Увы, официальный Toolbox что-то перестал скачивать обновления - видимо, высокая нагрузка.
P.S.: И вот ещё новая информация, о блокировании пользователей, которые уже оплатили лицензии.
Ого, решительно поддерживаю! Обещают, что приложение будет полностью функциональным и ни чем не отличаться от платной версии. Хорошее решение для пет-проектов и не только.
Злые языки утверждают, что такой шаг со стороны чехов - последствия снижения качества, низких продаж и жаркого дыхания VS и VSCode в спину Rider'у. Но я желаю парням успехов.
Ещё интернетах пишут, что эти IDE уже можно скачать через некий uTorrent. Не знаю, что это за приложение - надо попробовать. Увы, официальный Toolbox что-то перестал скачивать обновления - видимо, высокая нагрузка.
P.S.: И вот ещё новая информация, о блокировании пользователей, которые уже оплатили лицензии.
😁27👍9🔥3👏1🤯1
Атрибут DoesNotReturn #решение
Когда мы пишем оптимальный код, мы пытаемся вынести выброс Exception за пределы тела метода, чтобы упростить inline. Типичная ситуация выглядит следующим образом:
В этом случае статический анализатор (конечно, при включённом nullable annotations) вежливо подсказывает -
Некоторе, в подобной ситуации, кричат на код ("я знаю что делаю, дурацкая ты железка"), просто делая
Есть, однако, другое решение, которое я подсмотрел в BCL - атрибут DoesNotReturn. Этот атрибут указывает компилятору, что метод или свойство никогда не возвращает значение. Другими словами, он всегда создаёт исключение (выполнение вызывающего метода прекратится).
Обладая этими нехитрыми знаниями, мы просто добавляем атрибут
Более подробно про помощь компилятору можно прочитать тут и тут. Там же много других полезных атрибутов, которые помогут меньше кричать на код, а значит сделать его красивее и понятнее. Кажется, что все эти атрибуты особенно важны для библиотечного кода, тогда как в обычном коде они представляются мало востребованными.
Когда мы пишем оптимальный код, мы пытаемся вынести выброс Exception за пределы тела метода, чтобы упростить inline. Типичная ситуация выглядит следующим образом:
public object MyMethod() {
var data = ...
if (data == null) {
Error.MyError("msg");
}
return data;
}
public static Error {
public static void MyError(string msg) {
throw new Exception(msg);
}
}
В этом случае статический анализатор (конечно, при включённом nullable annotations) вежливо подсказывает -
data
может быть null
(см. скриншот). Это правильно и нормально, так как возвращаемое значение метода не отмечено как object?
, то есть оно не допускает значение null.Некоторе, в подобной ситуации, кричат на код ("я знаю что делаю, дурацкая ты железка"), просто делая
return data!
. Однако, когда код будет меняться и разрастаться, логика первоначального создателя может изменится или утратиться, а криков на код станет больше.Есть, однако, другое решение, которое я подсмотрел в BCL - атрибут DoesNotReturn. Этот атрибут указывает компилятору, что метод или свойство никогда не возвращает значение. Другими словами, он всегда создаёт исключение (выполнение вызывающего метода прекратится).
Обладая этими нехитрыми знаниями, мы просто добавляем атрибут
DoesNotReturn
для всех методов, которые выбрасывают ошибку и убираем крики на код. Просто и красиво.[DoesNotReturn]
public static void MyError(string msg) {
throw new Exception(msg);
}
Более подробно про помощь компилятору можно прочитать тут и тут. Там же много других полезных атрибутов, которые помогут меньше кричать на код, а значит сделать его красивее и понятнее. Кажется, что все эти атрибуты особенно важны для библиотечного кода, тогда как в обычном коде они представляются мало востребованными.
👍50🔥8🤨3
SkipLocalsInit #скорость
Мы часто используем
Это работает быстро, но можно сделать быстрее. В данном случае, местом ускорения будет создание буфера. В платформе есть правило - каждая переменная перед использованием должна быть инициализирована. В данном случае, платформа осуществляет инициализацию
Атрибут
Я бы не стал использовать этот атрибут в обычном коде, но для библиотечного кода (который, надеюсь, обложен тестами) этот подход может весьма существенно ускорить работу с промежуточными буферами.
Также, немаловажно то, что чем больше буфер, тем более выгодно брать его из
Больше подробностей о том, как работает инициализация в C# с
Код бенчмарка в комментариях.
P.S.: Если же мы хотим пропускать инициализацию модно и молодёжно, не помечая сборку как unsafe, мы должны использовать
Мы часто используем
Span
для ускорения сборки строк - создаём буфер на стеке, складываем туда кусочки будущей строки (чтобы не плодить промежуточные), а потом превращаем всё это в строку. Пример с ValueStringBuilder будет выглядеть следующим образом:public string MyMethod() {
Span<char> buffer = stackallock char[1024];
var vsb = new ValueStringBuilder(buffer);
...
}
Это работает быстро, но можно сделать быстрее. В данном случае, местом ускорения будет создание буфера. В платформе есть правило - каждая переменная перед использованием должна быть инициализирована. В данном случае, платформа осуществляет инициализацию
Span
, то есть заполняет его элементы значениями по умолчанию. Это достаточно ресурсоёмкая операция (см. Span против SpanSkipInit на бенчмарке), которую мы можем не делать, воспользовавшись атрибутом SkipLocalsInit. Атрибут
SkipLocalsInit
, применённый к методу, заставляет компилятор не использовать инициализацию локальных переменных этого метода. Важно понимать, что это не безопасная штука, так как в созданной переменной могут содержаться значения из памяти, которые были в памяти до этого. Именно поэтому, применение атрибута SkipLocalsInit
требует объявить всю сборку как unsafe
.Я бы не стал использовать этот атрибут в обычном коде, но для библиотечного кода (который, надеюсь, обложен тестами) этот подход может весьма существенно ускорить работу с промежуточными буферами.
Также, немаловажно то, что чем больше буфер, тем более выгодно брать его из
ArrayPool
. Это отчётливо видно на бенчмарке: буфер размером более 4096 char'ов уже стоит брать оттуда. Это понятнее для разработчиков и не требует применения unsafe
.Больше подробностей о том, как работает инициализация в C# с
SkipLocalsInitAttribute
, есть тут и тут.Код бенчмарка в комментариях.
P.S.: Если же мы хотим пропускать инициализацию модно и молодёжно, не помечая сборку как unsafe, мы должны использовать
Unsafe.SkipInit
.👍28❤5
Forwarded from .NET epeshk blog
🤡 Clown Object Heap — очередная новая куча в .NET 9 для вопросов на собеседованиях
Объекты в .NET делятся по четырём кучам. С двумя старыми приходится работать постоянно, про две относительно новые обычно вспоминают только на собесах (зачем про них спрашивают?)
Small Object Heap — обычная куча для объектов, разделённая на 3 поколения — нулевое, первое, и второе. С каждой сборкой мусора, выживший объект продвигается в следующее поколение. Сборки мусора в нулевом поколении быстрые и частые, во втором — медленные, блокирующие, редкие. Во время блокирующих сборок мусора содержимое SOH компактится (живые объекты копируются подряд, чтобы между ними не было дырок и память использовалась эффективно, ссылки на эти объекты обновляются, для этого и нужна блокировка потоков)
Large Object Heap — куча для больших объектов. Отличается тем, что по-умолчанию не компактится, т.к. копировать большие объекты долго. Иногда LOH считают абсолютным злом, но без него GC паузы при сборке мусора второго поколения стали бы совсем неприличными.
Основная проблема, связанная с LOH — фрагментация. Если аллоцировать короткоживущие объекты в LOH, особенно если размер объектов разный, после сборок мусора в LOH будут оставаться дырки. Далее окажется, что эти дырки слишком маленькие, чтобы заполнить их новыми объектами и программа начнёт раздуваться по памяти. Обычно проблема решается переиспользованием объектов
- в LOH попадают объекты размером >= 85000 байт
- в основном в LOH хранятся массивы, т.к. другие большие объекты в реальности не встречаются. Тем не менее, вручную можно сделать объект-не массив, который попадёт в LOH:
- в доисторическом фреймворке в LOH также попадают массивы
- можно попросить сборщик мусора скомпактить LOH:
- При запуске программы с memory limit LOH не может употребить всю доступную память. OutOfMemory случится до достижения лимита
- Популярные причины аллокаций в LOH: промежуточные коллекции, создаваемые внутри Linq методов, MemoryStream
Pinned Object Heap (.NET 5) — куча, объекты в которой сборщик мусора гарантированно никогда не перемещает. В ней нет аналога
Аллокация подобных объектов в обычной куче и их пиннинг (запрет на перемещение) через
Также ходят легенды, что доступ к массиву через указатель без bound checks (проверок на выход за границу массива) быстрее, но это скорее легенда, приводящая к багам, чем реальность.
Через публичный API в Pinned Object Heap можно создавать только массивы:
Объекты в POH собираются сборщиком мусора, и при неаккуратном использовании POH может стать фрагментированным.
Frozen Object Heap (NonGC Object Heap. .NET 8) — куча для объектов, которые живут всё время жизни программы, никогда не собираются сборщиком мусора, не ссылаются на объекты вне FOH, и никогда не изменяются. В ней хранятся объекты, создаваемые рантаймом дотнета: строковые литералы, объекты
@epeshkblog
Объекты в .NET делятся по четырём кучам. С двумя старыми приходится работать постоянно, про две относительно новые обычно вспоминают только на собесах (зачем про них спрашивают?)
Small Object Heap — обычная куча для объектов, разделённая на 3 поколения — нулевое, первое, и второе. С каждой сборкой мусора, выживший объект продвигается в следующее поколение. Сборки мусора в нулевом поколении быстрые и частые, во втором — медленные, блокирующие, редкие. Во время блокирующих сборок мусора содержимое SOH компактится (живые объекты копируются подряд, чтобы между ними не было дырок и память использовалась эффективно, ссылки на эти объекты обновляются, для этого и нужна блокировка потоков)
Large Object Heap — куча для больших объектов. Отличается тем, что по-умолчанию не компактится, т.к. копировать большие объекты долго. Иногда LOH считают абсолютным злом, но без него GC паузы при сборке мусора второго поколения стали бы совсем неприличными.
Основная проблема, связанная с LOH — фрагментация. Если аллоцировать короткоживущие объекты в LOH, особенно если размер объектов разный, после сборок мусора в LOH будут оставаться дырки. Далее окажется, что эти дырки слишком маленькие, чтобы заполнить их новыми объектами и программа начнёт раздуваться по памяти. Обычно проблема решается переиспользованием объектов
- в LOH попадают объекты размером >= 85000 байт
- в основном в LOH хранятся массивы, т.к. другие большие объекты в реальности не встречаются. Тем не менее, вручную можно сделать объект-не массив, который попадёт в LOH:
[StructLayout(LayoutKind.Sequential, Size = 84977)]
class LOHject { int _; }
Console.WriteLine(GC.GetGeneration(new LOHject())); // 2
- в доисторическом фреймворке в LOH также попадают массивы
double
от 1000 элементов. Сделано этого для оптимизации работы с double
на 32-битных системах, т.к. объекты в LOH на них выравнены по границе 8 байт. Этот факт полезен только на собеседованиях и квизах на конференциях- можно попросить сборщик мусора скомпактить LOH:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
- При запуске программы с memory limit LOH не может употребить всю доступную память. OutOfMemory случится до достижения лимита
- Популярные причины аллокаций в LOH: промежуточные коллекции, создаваемые внутри Linq методов, MemoryStream
Pinned Object Heap (.NET 5) — куча, объекты в которой сборщик мусора гарантированно никогда не перемещает. В ней нет аналога
CompactOnce
и нет требований к размеру объекта. Нужна для массивов, указатели на которые передаются в нативный код.Аллокация подобных объектов в обычной куче и их пиннинг (запрет на перемещение) через
fixed
или GCHandle
создаёт проблемы для компактящего сборщика мусора. Поэтому для них сделали отдельную кучу. Также ходят легенды, что доступ к массиву через указатель без bound checks (проверок на выход за границу массива) быстрее, но это скорее легенда, приводящая к багам, чем реальность.
Через публичный API в Pinned Object Heap можно создавать только массивы:
GC.AllocateArray<int>(128, pinned: true);
GC.AllocateUninitializedArray<int>(128, pinned: true);
Объекты в POH собираются сборщиком мусора, и при неаккуратном использовании POH может стать фрагментированным.
Frozen Object Heap (NonGC Object Heap. .NET 8) — куча для объектов, которые живут всё время жизни программы, никогда не собираются сборщиком мусора, не ссылаются на объекты вне FOH, и никогда не изменяются. В ней хранятся объекты, создаваемые рантаймом дотнета: строковые литералы, объекты
Type
, Array.Empty<>()
, ... Публичного API для создания объектов в FOH нет. Нужна для оптимизаций — 1) GC не просматривает эти объекты, 2) JIT может генерировать более эффективный код, зная что объект никуда не переместится, не изменится, и что о новых ссылках на него не нужно сообщать сборщику мусора@epeshkblog
👍24❤10🗿2