Pull to refresh
1
0
Send message

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

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

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]). Ну, чтож.

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

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

Про замыкания, ну бокс бог с ним - не будем называть это прямо "боксингом"

Не "не будем", а "нельзя".

Боксинг - хоть в .НЕТ, хоть в абстрактном CS-определении - это не "копировние из стека в кучу" и кроме аллокации тянет за собой и вычислительные расходы.

В случае с захватом этого не происходит.

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

но факт, что (возможно, нежелательное) копирование из стека в кучу при замыкании вполне возможно

Не "вполне возможно", а обязательно произойдёт, бай дизайн, так сказать.

только без личностей, хорошо? ... не знаю ваш возраст

Ноль вопросов, никаких личностей, просто я (лично) считаю, что учиться никогда не поздно и не зазорно, а иногда ещё и нужно ;)

Boxing возникает неявно на инструкции IL_0010

Вообще, не сказал бы, что прям неявно:

https://learn.microsoft.com/en-us/dotnet/api/system.reflection.emit.opcodes.constrained?view=net-8.0&redirectedfrom=MSDN

When a callvirt method instruction has been prefixed by constrained thisType, the instruction is executed as follows:

  • If thisType is a reference type (as opposed to a value type) then ptr is dereferenced and passed as the 'this' pointer to the callvirt of method.

  • If thisType is a value type and thisType implements method then ptr is passed unmodified as the 'this' pointer to a call method instruction, for the implementation of method by thisType.

  • If thisType is a value type and thisType does not implement method then ptr is dereferenced, boxed, and passed as the 'this' pointer to the callvirt method instruction.

This last case can occur only when method was defined on ObjectValueType, or Enum and not overridden by thisType. In this case, the boxing causes a copy of the original object to be made. However, because none of the methods of ObjectValueType, and Enum modify the state of the object, this fact cannot be detected.

Но ок, box в IL действительно нет, спасибо за напоминание.

Тем не менее, что насчёт обсуждаемого случая?

Есть там цитата?

Отлично, первые шаги к изучению учебника сделаны.

Вот, можете ознакомиться,

А можно теперь конкретную цитату, которая относится к обсуждаемому случаю?

boxing есть, но явная инструкция box в IL отсутствует.

Любопытно было бы увидеть примеры, не поделитесь?

...но это, к примеру, легко доступная элементарная оптимизация:

Объявляем локальную переменну, объявляем лямбду, которая её захватывает.

В цикле n раз изменяем значение переменной и вызываем метод, в который передаём лямбду - итого одна аллокация вместо n

Всё может быть чревато :)

С "проблемой цикла" можно научиться жить, а вот невозможность изменять захваченную переменную - мне бы мешало.

А что же по-вашему такое тогда боксинг? :-О

А вот это можно почитать и в учебнике.

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

Повторю: боксинга нет, ref struct захватывать нельзя по другой причине (да, именно из-за отъезда в кучу).

"Уезжать в кучу" != боксинг.

нет там никакого боксинга.

Большое спасибо.

А можете добавить в бенчмарки измерение аллокаций и GC?

Information

Rating
Does not participate
Registered
Activity