Обновить
-3

Пользователь

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

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

И что из этого неправда?

Я говорил именно про долгие CPU-bound таски, которые тредпул плохо переваривает.

Так а что имеется ввиду под "плохо переваривает долгие CPU-bound задачи"?

Вообще, долгая CPU-bound - это идеальная задача с точки зрения производительности, часто недостижимая.

О. не туда ответил, оказывается. См. ниже.

https://habr.com/ru/companies/skbkontur/articles/832742/comments/#comment_27113534

Просто утверждение было слишком общее и (как минимум поэтому) слишком некорректное.

С точки зрения перфоманса, несколько долгих задач будут гораздо эффективнее множества коротких - расходы (как процессора, так и памяти) на постановку задачи в очередь, создание стейт-машины, таска, регистрации комплишна, переключение контекста (опционально), GC, чтобы собрать весь этот мусор и ещё множества необходимых операций часто гораздо выше, чем время и ресурсы необходимые на выполнение самой полезной нагрузки.

Так что ещё раз: с точки зрения чистого перфоманса, несколько долгих задач будут гораздо выгоднее множества коротких.

А дальше, конечно, начинаются нюансы - что важнее, пропускная способность, общее время выполнения или "отзывчивость", сколько (много ли) задач нужно создать "авансом" или есть возможность организовать ограниченный буфер создаваемых задач (backpressure) и т.д., и т.п.

Но, повторюсь: с точки зрения чистой производительности главное правило такое: всё, что можно собирать в пакет и запускать как единый таск - должно собираться в пакет, а уж потом нужно смотреть, что из важных характеристик пострадало и корректировать.

чем задача дольше, тем хуже это может сказаться на перформансе

А можно немного раскрыть мысль?

...а где девиртуализация - там и возможный автоматический инлайнинг на уровне JITа.

А где структуры - там и локальность данных.

Нахлынули воспоминания ;)

Ускорение здесь линейно от количества вызовов горячего пути для получения результата. И если вызовов миллионы или миллиарды - запросто.

С#.

Если вкратце, то здесь два аспекта: девиртуальзация и обход боксинг.

С девиртуализацией всё просто: void Method(IInterface strategy) и void Method<T>(T strategy) where T : IInterface в рантайме будут скомпилированы в машкод по-разному. Вариант с дженериком может быть полностью девиртуализирован JITом (зависит от конкретной имплементации T, конечно, но исходим из того, что T написан правильно с точки зрения перфоманса). JIT в .нете становится умнее и старается девиртуализировать и интерфейсы/абстрактные классы/виртуальниые методы и даже делегаты на горячем пути, но это всё эвристики и без гарантий.

Боксинг: выше я писал "структуры всюду, где только можно (и имеет смысл)". "Всюду" означает как для данных, так и для логики, отличный пример - енумераторы. Стандартный пример - енумератор класса List<>, реализован как struct. Для того, чтобы он не был boxed, вызывающий код/JIT должны знать, что это List<T>.Enumerator, а не IEnumerator<T>, возвращаемый из IEnumerable<T>.GetEnumerator().

Таким же образом реализуем "стратегии" - struct имплементации интерфейсов IStrategy. new Struct()/default(Struct) создаётся на стеке, не нагружает GC. Главное, избежать боксинга, а значит передавать не как IStrategy, а как T where T : IStrategy.

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

Так получаются "десятки генерик-аргументов" о которых я писал. "Десятки", наверное, перебор, но по 10+ у нас бывало.

...про кэш-лайны и локальность данных ещё забыл ;)

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

Конретно у нас "хаки" были вроде девиртуализации любыми способами (отказ от интерфейсов, виртуальных методов, делегатов, десятки генерик-аргументов - правда генерики в том числе для обхода возможного боксинга), структуры всюду, где только можно (и имеет смысл) в обмен на удобство наследования и переиспользование кода, поиск оптимальных размеров структур и хаки с паддингами/выравниванием, небезопасные балк-операции прямо над памятью вместо обращению к полям/пропертям, ручной инлайнинг повсюду (привет дублированию кода), небезопасное приведение типов (практически всюду), обход проверок границ, арифметика с указателями, ин-проц и ин-мемори всего, что только возможно, практически весь горячий путь не следовал вообще никаким "хорошим паттернам и практикам", а точнее - весь был написан вопреки.
Это только первое, что пришло на ум. Хватит? ;)

Алгоритмы и архитектура (уровня классов/модулей/внутреннего дата-флоу, в первую очередь) тоже, конечно.

Доля "хаков" в приросте производительности была действительно львиная.

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

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

Или ускорение превью рана с минут до секунд(ы).

Что, если не это пример кардинального улучшения юзер экспириенса и экономии.

А в чём суть возражений?

...что-то заигрался я в бенчмарки, но тут интересное:

NET8
NET8

При добавлении заранее отсортированных элементов у SortedList скорость ожидаемо растёт, а у SortedDictionary - неожиданно падает. Хотя если подумать - слишком часто приходится перебалансировать дерево.

В общем, нюансов у этих Sorted* масса, как я уже писал: если нужна производительность - скорее всего придётся писать своё специализированное.

А, ну и да - SortedDictionary выделяет память в куче при каждом добавлении - это означает, что его не имеет особого смысла пулить/переиспользовать, а вот SortedList - стоит.

Плюс у SortedList можно установить Capacity, и он не будет выделять новые массивы при добалении элементов в рамках установленной capacity.

Ну и ещё одна мелочь - внутри у SortedList не список, а два массива - ключи и значения.

SortedList может выиграть (за счет более компактного представления в памяти) в сценарии, когда

SortedList выигрывает в любом сценарии, когда нам нужна именно отсортированная коллекция - он гораздо лучше при перечислении (плюс предоставляет доступ по индексу). SortedDictionary при каждом вызове GetEnumerator создаёт Stack и в дальнейшем его использует, что может приводить к выделению одного или более массивов в куче - в зависимости от глубины дерева.

Если же нам не нужен доступ перечислением - то нам не нужны и SortedList/SortedDictionary, логично?

Но и при добавлении и поиске в NET6/NET7 SortedList БЫЛ лучше - я поднял некоторые свои бенчмарки и обнаружил, что в NET8 SortedDictionary наконец-то стал быстрее при добавлении и поиске - приятный сюрприз, нужно будет посмотреть, что они изменили.

О чём я ещё забыл и сейчас обнаружил - SortedList тоже выделяет память в куче при каждом вызове GetEnumerator - но выделяет константный объём, 48 байтов. Не помню и не смотрел, что именно он выделяет, но это в любом случае лучше, чем Stack в SortedDictionary.

Результат бенчмарка для NET8 и NET7:

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3007/23H2/2023Update/SunValley3)
12th Gen Intel Core i9-12900HK, 1 CPU, 20 logical and 14 physical cores

.NET SDK 8.0.100


[Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2

| Method                                            | Mean          | Error        | StdDev       | Gen0   | Gen1   | Allocated |
|-------------------------------------------------- |--------------:|-------------:|-------------:|-------:|-------:|----------:|
| SortedDictionary_Reused_16_Enumerate              |      95.47 ns |     1.599 ns |     1.496 ns | 0.0095 |      - |     120 B |
| SortedList_Reused_16_Enumerate                    |      26.42 ns |     0.217 ns |     0.203 ns | 0.0038 |      - |      48 B |
| SortedDictionary_Reused_1024_Enumerate            |   5,530.26 ns |    31.941 ns |    28.315 ns | 0.0153 |      - |     216 B |
| SortedList_Reused_1024_Enumerate                  |   1,307.96 ns |    14.057 ns |    13.149 ns | 0.0038 |      - |      48 B |
| SortedDictionary_Reused_Random_Add_16             |     246.21 ns |     1.989 ns |     1.861 ns | 0.0610 |      - |     768 B |
| SortedList_Reused_Random_Add_16                   |     290.66 ns |     4.571 ns |     4.275 ns |      - |      - |         - |
| SortedDictionary_Reused_Random_Add_16_Get_16      |     380.68 ns |     2.345 ns |     2.193 ns | 0.0610 |      - |     768 B |
| SortedList_Reused_Random_Add_16_Get_16            |     494.61 ns |     6.040 ns |     5.650 ns |      - |      - |         - |
| SortedDictionary_Reused_Random_Add_16_Get_160     |   1,208.38 ns |    10.448 ns |     9.773 ns | 0.0610 |      - |     768 B |
| SortedList_Reused_Random_Add_16_Get_160           |   1,867.62 ns |    14.080 ns |    13.171 ns |      - |      - |         - |
| SortedDictionary_Reused_Random_Add_1024           |  56,606.91 ns |   276.384 ns |   258.530 ns | 3.9063 | 0.4883 |   49152 B |
| SortedList_Reused_Random_Add_1024                 |  74,034.43 ns |   414.384 ns |   387.615 ns |      - |      - |         - |
| SortedDictionary_Reused_Random_Add_1024_Get_1024  | 102,770.04 ns |   334.855 ns |   296.840 ns | 3.9063 | 0.4883 |   49152 B |
| SortedList_Reused_Random_Add_1024_Get_1024        | 132,839.35 ns |   514.959 ns |   481.693 ns |      - |      - |         - |
| SortedDictionary_Reused_Random_Add_1024_Get_10240 | 507,017.59 ns |   922.908 ns |   818.134 ns | 3.9063 |      - |   49154 B |
| SortedList_Reused_Random_Add_1024_Get_10240       | 570,572.80 ns | 2,884.269 ns | 2,408.496 ns |      - |      - |       2 B |




[Host] : .NET 7.0.14 (7.0.1423.51910), X64 RyuJIT AVX2

| Method                                            | Mean           | Error       | StdDev      | Gen0   | Gen1   | Allocated |
|-------------------------------------------------- |---------------:|------------:|------------:|-------:|-------:|----------:|
| SortedDictionary_Reused_16_Enumerate              |       162.2 ns |     2.18 ns |     2.04 ns | 0.0095 |      - |     120 B |
| SortedList_Reused_16_Enumerate                    |       102.1 ns |     0.36 ns |     0.30 ns | 0.0038 |      - |      48 B |
| SortedDictionary_Reused_1024_Enumerate            |    10,244.9 ns |   181.27 ns |   169.56 ns | 0.0153 |      - |     216 B |
| SortedList_Reused_1024_Enumerate                  |     4,827.5 ns |    11.26 ns |     9.98 ns |      - |      - |      48 B |
| SortedDictionary_Reused_Random_Add_16             |       426.9 ns |     4.32 ns |     3.83 ns | 0.0610 |      - |     768 B |
| SortedList_Reused_Random_Add_16                   |       335.6 ns |     5.09 ns |     4.76 ns |      - |      - |         - |
| SortedDictionary_Reused_Random_Add_16_Get_16      |       813.4 ns |     9.89 ns |     8.77 ns | 0.0610 |      - |     768 B |
| SortedList_Reused_Random_Add_16_Get_16            |       479.7 ns |     9.32 ns |     9.57 ns |      - |      - |         - |
| SortedDictionary_Reused_Random_Add_16_Get_160     |     3,838.5 ns |    12.82 ns |    12.00 ns | 0.0610 |      - |     768 B |
| SortedList_Reused_Random_Add_16_Get_160           |     2,117.7 ns |    37.54 ns |    35.11 ns |      - |      - |         - |
| SortedDictionary_Reused_Random_Add_1024           |   101,679.9 ns |   323.19 ns |   269.88 ns | 3.9063 | 0.4883 |   49152 B |
| SortedList_Reused_Random_Add_1024                 |    78,013.1 ns |   345.91 ns |   288.85 ns |      - |      - |         - |
| SortedDictionary_Reused_Random_Add_1024_Get_1024  |   202,480.1 ns |   745.23 ns |   660.63 ns | 3.9063 | 0.4883 |   49152 B |
| SortedList_Reused_Random_Add_1024_Get_1024        |   135,209.1 ns |   662.28 ns |   619.49 ns |      - |      - |         - |
| SortedDictionary_Reused_Random_Add_1024_Get_10240 | 1,069,533.8 ns | 8,001.54 ns | 7,093.16 ns | 3.9063 |      - |   49155 B |
| SortedList_Reused_Random_Add_1024_Get_10240       |   609,317.3 ns | 2,603.99 ns | 2,174.45 ns |      - |      - |       2 B |

Накладными расходами, очевидно.

В качестве обмена опытом: в сценариях, где важна [даже относительно] высокая производительность SortedDictionary не подходит: производительность добавления ожидаемая, но на каждое добавление происходить выделение памяти, скорость поиска ожидаемо невысокая, перечисление медленное, при перечислении выделяет память.

SortedList, к слову, тоже так себе. Не помню уже кто был быстрее, кто медленнее, помоему SortedList побыстрее, но точно размещает меньше индивидуальных инстанцев в куче.

При необходимости рекомендую проанализировать сценарий и написать свой специализированный компонент.

С другой стороны, в клиентском приложении на ЮИ не на горячем пути вполне применим.

Поддерживаю вопрос товарища @dopusteam

У меня есть несколько догадок касательно того, что имелось ввиду, но ни в одной не уверен.

Локальная переменная теперь оказывается не работает как локальная, но работает как метод.

А когда локальная переменная передаётся как ref - она перестаёт работать как локальная?

Для рантайма она больше не локальная переменная, но это скрыто и незаметно (пока не упираемся в особые случаи типа ref [struct]). Ну, чтож.

Возможно не стоит, а возможно и стоит - написание руками класса с полем и методом, повторяющим то, что сделал бы компилятор для лямбды - это разумно/упрощает/стоит того?

Я склоняюсь к тому, что универсального ответат нет, зато выбор есть.

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность