Dictionary: очень специальный

Как-то раз была поставлена задача ускорить работу с Dictionary, где ключом всегда выступал int, а значением — структура. Имеющаяся скорость не устраивала. Более того, очень бы хотелось иметь возможность получать ссылку (ref) на значение в Dictionary, чтобы можно было изменять содержимое извне. В настоящий момент полнофункциональный словарь из dotnet такого поведения не поддерживает.

В статье,  как и в предыдущей, речь пойдёт о наносекундах и экономии байтиков. Уверен, что 99% программистов этого не нужно, а подобные эксперименты без изучения environment’a будут даже опасны. Однако, если ваш профиль high-load или геймдев, данная информация может быть востребована.

Как проектируем?

Так как всё, что делает этот словарь связано со структурами, было решено сделать и его структурой. Это чуть-чуть уменьшит время, которое мы будем тратить на вызов методов — runtime’у не надо будет выяснять по таблице виртуальных методов, кому принадлежит этот метод. Это, в свою очередь, облегчит inline методов — одну из самых эффективных методик оптимизации скорости. Ну, если мы говорим о наносекундах, конечно.

Далее, мы не будем изобретать велосипед и воспользуемся всеми наработками dotnet-community для того, чтобы реализовать словарь. Так как ключ это всегда int, мы можем смело выбрасывать всё, что касается сравнения ключей и получения hash-кода. Для того, чтобы размер внутренней коллекции совпадал с вычисленными эмпирическим путём значениями, будем использовать и вот этот класс.

Добавление элементов в словарь

Метод добавления очень похож на метод из настоящего Dictionary. С одним исключением: мы можем, например, добавить ключ и сразу получить ссылку на значение во внутренней коллекции словаря. Классическое добавление тоже, конечно, присутствует.

Если запустить benchmark и измерить скорость добавления элементов, то мы увидим интересную картину:

Benchmark: C# add to Dictionary<int,int> vs custom special dictionary» /></p>

<p>Benchmark: C# add to Dictionary<int,int> vs custom special dictionary</p>

<p>Во-первых, весьма очевидно, что Dictionary в .NET 6 стал заметно шустрее. Это очень радует, команда dotnet — молодец.</p>

<p>Также, специальная имплементация закономерно выигрывает на старых framework’ах. Относительно высокая скорость добавления обусловлена тем, что мы избавились от лишних вызовов EqualityComparer’a, плюс мы используем способ работы с внутренним массивом из Dictionary<int,int>.Enumerator. Видимо ранее разработчики основной библиотеки, к сожалению, не могли произвести все нужные оптимизации. Теперь — всё красиво.</p>

<p>Ну и причины, по которой мы сэкономили немного памяти просты — во внутренней структуре Entry отсутствует поле hashcode, потому что оно не нужно при ключе из int.</p>

<h3>Перечисление элементов словаря</h3>

<p>Простой benchmark покажет нам, что использовать внутреннюю структуру словаря без использования промежуточного KeyValuePair достаточно эффективно. Плюс, немного производительности добавляет ref struct.</p>

<p><img src=

Benchmark: C# get value from Dictionary vs custom special dictionary

Результаты, в принципе, ожидаемые, поскольку алгоритм не нуждается в сравнении с использованием EqualityComparer, а значит убираются лишние вызовы и код становится более предсказуемым для процессора.

Из интересного: для того, чтобы иметь возможность отдавать ref в методе GetValue, приходится использовать дополнительное статическое поле, которое содержит значение по-умолчанию. Оно никогда не вернётся, так как чуть выше находится Exception (сам он кидается в методе статического класса Errors). Мотивы, которые побудили меня сделать так описаны в предыдущей статье про inline и throw.

Следующий тест — читаем и записываем новый результат в словарь. Это то самое, ради чего всё затевалось.

Benchmark: C# get write to Dictionary vs custom special dictionary

Benchmark: C# get write to Dictionary vs custom special dictionary

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

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

Выводы

  1. Иногда стоит писать свои коллекции. Даже такие сложные, как Dictionary. Тем более, что основной алгоритм поиска значения по ключу давно известен и даже лежит на github.

  2. Иногда не стоит писать свои коллекции. Да-да, именно так. Не смотря на то, что результаты достаточно впечатляющие, они не гарантируют на 100%, что покрыты абсолютно все случаи добавления/удаления данных из коллекции. А что, если множественное добавление и удаление понизит производительность? А как быть с многопоточностью? Если вам хочется отвечать на все эти вопросы — вперёд. Это интересно.

P.S.: Начал писать в телегу про производительность. Заглядывайте, если интересно.

© Habrahabr.ru