Меня зовут Женя, я работаю в Itransition как backend .Net developer – и это моя первая статья на техническую тему. Сегодня я хочу рассказать о типах словарей, которые бывают в .Net, а также о Hash table, о сложности поиска и вставки, о применимости в различных условиях, и даже слегка о Tree. Для самого начала нам нужно хотя-бы вскользь пробежаться по алгоритмической сложности. Звучит заумно, но на практике обычно не вызывает сложностей. Мы не будем строить графики и выдвигать математическое доказательство почему O(1) лучше O(n), но нам нужно в самом начале понять, что O(1) означает. Сложность алгоритмов оценивается по двум критериям – временная и по затрачиваемой памяти, потому что и время, и память не безграничны, и мы хотим понять как много мы будем их тратить в сравнении одного и другого пути развития. Нам на данном этапе понадобиться только 3 из них: O(1) – самый желанный, простой в понимании и сложный в достижении. Означает, что при неограниченно растущей нагрузке мы будем делать то, что нам требуется за условную единицу времени, эта единица времени может быть и секунда и день, но глобально это одно и то же время для любой нагрузки, которое будет испытывать алгоритм. O(n) – гораздо хуже O(1), но часто приемлемый, а иногда максимально возможный. Означает, что единица времени растет линейно с ростом нагрузки, примером может служить АЗС – в единицу времени колонка качает 1 л. топлива: чтобы перекачать 100 литров, нам нужно в 100 раз больше колонок с той же производительностью. O(logN) – опять же, хуже O(1), но лучше O(n). Чтобы понять, лучше почитать больше литературы, связанной со сложностью. Но если очень приближенно, то смысл в том, что алгоритм на каждом этапе дает выигрыш в 2 раза, центральная идея бинарных деревьев основана именно на этом (к деревьям мы еще вернемся чуть ниже), данная сложность дает большой профит на большой объеме данных. Начнем рассказ о HashTable – данная структура доступна для использования и сейчас (.net6) и как любой объект его можно создать: var hashTable = new Hashtable(); и мы можем даже что-то там хранить в виде пары ключ-значение: hashTable.Add("company", "Itransition"); hashTable.Add(56, "numberKey"); Но тут мы получаем проблему использования, а именно: и ключ и значение не типизированы, и могут быть чем угодно, что обеспечивается тем, что и ключ, и значение хранится в виде object, что приводит к проблеме boxing/unboxing. А это дополнительные расходы ресурсов – как памяти, так и времени. В общем, в явном виде им уже давно никто не пользуется. Любой разработчик .Net, который слышит слово Dictionary, представляет себе конструкцию вида: var dictionary = new Dictionary<TKey, TValue>(); где TKey – это ключ, а TValue – значение, которое соответствует этому ключу. Использование такой структуры данных очень эффективно с точки зрения скорости поиска, вставки и объема используемой памяти. В среднем сложность поиска и вставки составляет O(1), в худшем же O(n) – почему это так, останется за рамками данной статьи. Чтобы добиться такого шикарного результата, dictionary внутри себя использует HashTable, но в свою очередь dictionary типизирован, т.е. если пользователь создал dictionary для хранения ключей тип число, а для значений объекты котиков, использовать строки он уже не сможет – компилятор будет против) – и это убирает проблему boxing/unboxing, что существенно может сэкономить дополнительные накладные расходы.Если не используется многопоточность, то dictionary является шикарным выбором для хранения и получения сохраненного результата. Вот это штука очень крутая – в мире, когда все хотят многопоточный код, этот вид dictionary как эликсир жизни и скорости в одном флаконе. Реализуется потокобезопасность с помощью механизма Monitor, который вешает lock на запись значения по ключу, что в свою очередь и закрывает проблему одновременной записи в словарь двух одинаковых значений одновременно. Как раз из-за того, что нет гарантии, что значение было записано в названия методов, добавлено Try – TryAdd(), TryUpdate(), TryDelete()Но вдруг вы хотите усложнить себе жизнь, тогда для вас есть  Hashtable.Synchronized(hashTable); Данный HashTable также потокобезопаснен, но хранит и ключ, и значение в виде object – и опять же мы встречаемся с проблемой boxing/unboxing. Как и ConcurrentDictionary, данный вид dictionary является потокобезопасным, т.к. не существует проблемы одновременной записи, мы можем только получать сохраненные значения. Если посмотреть, то у данного dictionary есть методы добавления новых ключ-значение, но на самом деле этого не происходит, потому что при добавлении новой записи в ImmutableDictionary, мы получим новый словарь с копией всех данных из первого словаря, плюс еще одна запись, а исходный не изменится. А самое интересное, что внутри это не HashTable, а AVL Tree, для которого скорость поиска и вставки составляет уже O(logN). Что такое AVL Tree и деревья в общем, останется за пределами данной статьи. Мы никогда не должны основываться на «мне так кажется»: цифры лучше показывают реальность, нежели ощущения. Поэтому нужно измерять: все что измерено, то оторвано от стереотипов. Для себя я использую BenchmarkDotNet – пакет для .net приложений, позволяющий очень быстро и просто оценить: лучше/хуже стало и на сколько. За примерами использования лучше обратиться к документации или посмотреть в код. Для моей задачки показать, чем отличаются словари, этот инструмент подходит идеально. Для всех тестов будет использоваться .NET SDK=6.0.101 Для начала давайте посмотрим, как словари справляются с вставкой 100, 1000 и 10000 значений Method Target Mean Error StdDev Rank Allocated Dictionary_Add 100 11.50 us 0.036 us 0.032 us 1 11 KB Hashtable_Add 100 16.33 us 0.282 us 0.325 us 2 14 KB ConcurrentDictionary_Add 100 20.34 us 0.072 us 0.060 us 3 25 KB ImmutableDictionary_Add 100 62.62 us 0.900 us 0.842 us 4 49 KB Dictionary_Add 1000 118.59 us 0.398 us 0.372 us 5 114 KB Hashtable_Add 1000 162.68 us 0.968 us 0.905 us 6 140 KB ConcurrentDictionary_Add 1000 209.76 us 2.043 us 1.811 us 7 220 KB ImmutableDictionary_Add 1000 903.69 us 9.405 us 7.854 us 8 723 KB Dictionary_Add 10000 1,254.97 us 11.560 us 10.248 us 9 1,051 KB Hashtable_Add 10000 1,903.21 us 27.632 us 25.847 us 10 1,335 KB ConcurrentDictionary_Add 10000 2,583.28 us 43.599 us 40.782 us 11 1,782 KB ImmutableDictionary_Add 10000 12,779.01 us 206.515 us 183.070 us 12 9,601 KB Видно, что обычный Dictionary 100 записей вставляет за 12 наносекунд, а 1000 примерно за 120.от мы и получили почти O(1) сложность вставки одной записи! HashTable чуть медленнее, но он хранит и возвращает все в object и нет проверки на уровне компиляции. ConcurrentDictionary еще немного медленнее – это связано с накладными расходами, обеспечивающими потокобезопасность, но очень даже неплохо для такой мощной системы! Самый же медленный – это ImmutableDictionary. еще прошу обратить внимание на потребляемую им память: при каждой итерации она значительно больше, чем у всех собратьев. Это связано с тем, что при каждой вставке в новый словарь копируются все значения из старого словаря и еще одно, которое мы хотим добавить. Еще прошу заметить, насколько он медленнее при вставке новой записи – все потому что данные в нем хранятся без использования HashTable, а с использованием AVL Tree, у которого сложность вставки O(logN).Теперь предлагаю посмотреть, как словари справляются с поиском из 1000 значений при 100, 1000 и 10000 попытках. Method Target Mean Error StdDev Rank Gen 0 ConcurrentDictionary_Get  100      9.276 us   0.0784 us   0.0695 us  1        -  Dictionary_Get  100      9.374 us   0.0549 us   0.0487 us  1        -  Hashtable_Get  100     10.651 us   0.0537 us   0.0476 us  2 0.7629 ImmutableDictionary_Get  100     14.019 us   0.0436 us   0.0340 us  3        -  ConcurrentDictionary_Get  1000     84.548 us   0.2929 us   0.2596 us  4        -  Dictionary_Get  1000     90.155 us   0.3177 us   0.2817 us  5        -   Hashtable_Get  1000    108.548 us   0.4798 us   0.4253 us  6 7.5684  ImmutableDictionary_Get  1000    140.010 us   0.2568 us   0.2276 us  7        -   ConcurrentDictionary_Get  10000    891.774 us   3.0775 us   2.7281 us  8        -   Dictionary_Get  10000    936.266 us   2.9327 us   2.7433 us  9        -   Hashtable_Get  10000  1,082.217 us   7.7229 us   6.8461 us  10 76.1719  ImmutableDictionary_Get  10000  1,391.331 us   4.9817 us   4.4161 us  11        -  Тут видно, что время поиска у Dictionary и ConcurrentDictionary практически одинаковое. Оно на самом деле является одинаковым в пределах погрешности и незначительно изменяется при разных прогонах. Поэтому можно считать, что скорость поиска, в отличие от вставки, у обеих коллекций одинаковая и она очень хорошая, даже отличная! Для HashTable можно заметить, что выделяется память, а в других словарях такого нет – это как раз связано с тем, что HashTable хранит и ключ, и значение в виде object, и есть накладные расходы на boxing/unboxing  последующую чистку возникшего мусора с помощью Garbage Collection. Часто это не тривиально и просто так не увидеть, читая код, поэтому еще раз призываю Вас измерять свой код на производительность. И самый медленный из всех опять же ImmutableDictionary, что наводит на следующую мысль: нужно очень внимательно выбирать структуры данных для использования, так есть области применения, где ImmutableDictionary раскрывается с наилучшей стороны – там, где нужно чтобы коллекция была неизменяема – вот так поворот! Для завершения расскажу, как можно взять отличный Dictionary (я надеюсь, что к этому моменту вы уже с уважением относитесь к этой структуре данных) и испортить его функционал поиска в 257 раз! Будем сравнивать поиск по ключу через метод TryGetValue, ContainsKey, ручной поиск по ключу, Linq.FirstOrDefault. Взглянем на результаты:  Method Mean Error StdDev Ratio RatioSD Gen 0 GetValueByKeyWithTryGet       7.550 ns    0.0767 ns    0.0718 ns  0.96 0.02       -  GetValueByKey       7.864 ns    0.1307 ns    0.1158 ns  1 0       -  GetValueByKeyManual     571.320 ns    7.4372 ns    6.5929 ns  72.66 1.37       -  GetValueByKeyWithLinq   1,839.986 ns   30.7720 ns   32.9257 ns  234.3 5.68 0.0343 Вот это да!!! Между TryGetValue и FirstOrDefault разница в 257 раз – и это на словаре в 100 записей, и дополнительно нужно выделять память и еще создавать работу для GC, подчищая за нами. С умом подходите к использованию методов.Репозитоторий для тех, кому интересно запустить у себя.