Хабр Курсы для всех
РЕКЛАМА
Практикум, Хекслет, SkyPro, авторские курсы — собрали всех и попросили скидки. Осталось выбрать!
Если свободное место в словаре закончилось, то алгоритм обычно решает удвоить текущую емкость
То есть для хранения такой сущности не выделяется отдельное место в куче, она помещается по месту объявления, то есть в данном случае в области памяти, занимаемой массивом _entries.
Разработчик должен представлять, как устроен словарь, как он работает с памятью, насколько затратен, сколько «следов» оставит в поколениях и LOH, вызывая излишнюю нагрузку на сборщик мусора; как его лучше инициализировать, в каких случаях вместо него лучше использовать коллекцию пар ключ-значение, в каких – сортированную коллекцию и т.д.И помнить, разумеется, наизусть IL код для него. Скажу сразу, такие заявления — повод подробно пройтись по статье.
Это заметно лучше, чем в реализации ConcurrentDictionary, где аналогичная сущность представлена ссылочным типом.Что значит «лучше»? Почему лучше?
То есть для хранения такой сущности не выделяется отдельное место в куче, она помещается по месту объявления, то есть в данном случае в области памяти, занимаемой массивом _entries.… а на массив выдаеляется место в куче.
По графику видно, что стратегия заполнения словаря по умолчанию привела к резервированию 400МБ памяти для хранения 10млн. значений (80МБ полезных данных). Но если задать емкость при вызове конструктора new Dictionary<int, int>(10001000), то для хранения этих же данных будет зарезервировано всего 190МБ.Нет, по графику этого не видно. Вообще неясно, какая там стратегия была. И что вообще значит «стратегия по умолчанию»? И зачем её использовать для инициализации больших списков?
Модифицируем предыдущий опыт такА какой был предыдущий опыт?
Как видно, время поиска с перебором элементов массива линейно растет от 18 наносекунд при count=10 до 134 нс при count=190Я теряю кейс статьи. Только что графики были на миллионы записей, а теперь — на десятки. И совете (выводе?) в конце тоже про «пятьдесят записей».
помним, поиск в словаре при идеальных условиях константныйНет, не помним. Сложность поиска константна. И не при идеальных условиях, а всегда. Константная сложность здесь означает, что значение будет найдено за конечное количество шагов, не зависящее от размера коллекции.
Причиной такого поведения является упорядоченность перебора при поиске по массиву, что делает этот поиск предсказуемым для процессораДа причём тут процессор вообще? Это сложность алгоритма. Можно руками всё посчитать, без процессора, с тем же успехом. Просто каждый шаг вручную будет дольше считаться.
Другой причиной можно назвать заложенную в словарь гибкость, с использованием вызова виртуальных функций (callvirt для GetHashCode и Equals)Причём тут вообще словарь и «заложенная в нём гибкость»? Просто GetHashCode и Equals — виртуальные функции...
Кстати, в некоторых случаях, если требования к скорости работы алгоритма высоки, можно рассмотреть вопрос о самостоятельной переработке словаря с заменой обобщенного (generic) ключа на ключ конкретного типа.Теряюсь в догадках, что это может дать. Так как материализованные generic типы тоже имеют ключ конкретного типа.
ключ неуправляемого значимого типаНеуправляемый тип?
Во-вторых, добавлять миллионы элементов по одному — ну я не знаю, для чего это тестировать и делать какие-то выводы.
А в словарь по-другому их и не добавить же. Даже копирующий конструктор так работает.
Да причём тут процессор вообще?
Притом, что исполнение кода на процессоре имеет свои особенности, которых не возникает если считать "руками" и без процессора.
А в словарь по-другому их и не добавить же. Даже копирующий конструктор так работает.Да, тут я затупил.
Притом, что исполнение кода на процессоре имеет свои особенности, которых не возникает если считать «руками» и без процессора.И это никак не влияет на сложность алгоритма.
Что значит «лучше»? Почему лучше?
… а на массив выдаеляется место в куче.
И зачем её использовать для инициализации больших списков?Заметка как раз об этом. Не все разработчики понимают, что стоит за некоторыми внешне вполне безобидными строчками кода.
Во-вторых, добавлять миллионы элементов по одному — ну я не знаю, для чего это тестировать и делать какие-то выводы.А почему нет? Со словарем обычно именно так и работают.
А какой был предыдущий опыт?Представлен на предыдущем графике.
Я теряю кейс статьи. Только что графики были на миллионы записей, а теперь — на десятки. И совете (выводе?) в конце тоже про «пятьдесят записей».Угу. Невнятно написал, согласен. В заметке привел несколько наглядных опытов и выводов по ним. Общих выводов в конце нет, только по последнему опыту.
Нет, не помним. Сложность поиска константна. И не при идеальных условиях, а всегда. Константная сложность здесь означает, что значение будет найдено за конечное количество шагов, не зависящее от размера коллекции.Не означает. Для поиска по хэш-таблицы нет никаких гарантий, что он не выродится в пробег по всей сохраненной коллекции, есть лишь ожидание, что этого не произойдет.
Да причём тут процессор вообще?Там написано про поиск по массиву. Это один из опытов. При определенном количестве элементов поиск по массиву быстрее. Написано, почему.
Теряюсь в догадках, что это может дать. Так как материализованные generic типы тоже имеют ключ конкретного типа.
Неуправляемый тип?
Хранение ссылочного типа накладно.А для value типа — копирование. Которое произойдёт при изменении внутренних коллекций.
Заметка как раз об этом.Тогда это нужно указать явно. А также описать, за счёт чего во втором варианте получился выигрыш по памяти более чем в 2 раза.
Представлен на предыдущем графике.Я не хочу обидеть, но мне кажется, мы с вами какие-то разные графики видим. У вас там вроде как на графике и способ добавления есть, и описаны значения, используемые для ключа и значения. Я ничего этого не вижу.
Не означает. Для поиска по хэш-таблицы нет никаких гарантий, что он не выродится в пробег по всей сохраненной коллекции, есть лишь ожидание, что этого не произойдет.Означает) Приведёте миллионную хэштаблицу, у которой поиск вырождается? Если серьёзно, то О большое по определению ограничивает алгоритм сверху. Так что это — по определению! — гарантия, что ничто никуда не выродится. Да, если у вас пять записей, можно натянуть на это «выродится». Но это по-прежнему означает «конечное количество шагов, независимо от размера коллекции».
Там написано про поиск по массиву. Это один из опытов. При определенном количестве элементов поиск по массиву быстрее. Написано, почему.Да, я видимо решил, что та фраза связана с «помним, поиск в словаре при идеальных условиях константный». Так или иначе, нужно понимать, какой конкретно процессор имеется в виду, а также, что от процессора к процессору это значение будет меняться.
Использование явного типа вместо обобщенного позволяет уйти от callvirt, что в свою очередь увеличивает производительность поиска по словарю процентов на 10-20. В некоторых проектах это достаточно важный плюс.Скиньте ссылку, пожалуйста, на документацию, этого я не знал. А какие именно методы перестают быть виртуальными?
А для value типа — копирование. Которое произойдёт при изменении внутренних коллекций.Ну значит желательно проектировать архитектуру так, чтоб таких изменений было меньше. Заметка как раз об этом.
Тогда это нужно указать явно. А также описать, за счёт чего во втором варианте получился выигрыш по памяти более чем в 2 раза.Это все указано и написано
Приведёте миллионную хэштаблицу, у которой поиск вырождается?Да хоть миллиардную). Банально переопределите у ключа
public override int GetHashCode()
{
return 1;
}
Так или иначе, нужно понимать, какой конкретно процессор имеется в виду, а также, что от процессора к процессору это значение будет меняться.Исследование разных конфигураций не является темой этой заметки. В остальном же для актуальных процессоров это правило.
Скиньте ссылку, пожалуйста, на документацию, этого я не знал. А какие именно методы перестают быть виртуальными?Я такой не знаю. Вроде было что-то когда-то в msdn, но кануло. А специально эту тему не исследовал. Просто посмотрел на IL код FindEntry для framework и core. Во втором случае есть определенные оптимизации для типов значений и они действительно дают измеримый эффект (впрочем это не тема данной заметки, просто увидел во время тестов).
и выродитсяТогда ясно, что у вас имелось в виду под «идеальным сценарием» — это когда используется настоящая хэш-функция, а не одно число на все значения.
Это как раз и есть худший случай для оценки сложности алгоритма.Если оценивать алгоритм по такому случаю, то он будет хуже полного перебора, т.е. хуже O(n) за счёт сложной внутренней структуры. Однако, в прикладных задачах нет смысла опираться на этот случай.
На миллион ключей может быть как миллион уникальных хэшей, так и множество совпадающих.И это «множество» совпадающих всё равно будет существенно меньше исходных миллионов в коллекции. Именно потому и O(1).
Сложность поиска константна. И не при идеальных условиях, а всегда.
Для любой реализации с ограничением количества бакетов асимптотическая сложность среднего или максимального времени поиска не в лучшем случае не будет константной. Просто потому, что всегда можно запихать такое количество элементов, что это среднее превысит наперёд заданную константу.Я ещё раз, на всякий случай, повторю.
В нашем случае O(1) — это функция вида y=C.1. Чисто из любопытства, O(n3) (в кубе) — это функция какого вида? То есть, какое максимальное количество шагов — следуя вашему определению — должно быть гарантировано алгоритмом?
В нашем случае O(1) — это функция вида y=C. Согласно этому определению, если у алгоритма такая сложность — то найдётся такое C (константа), что при любом количестве элементов количество шагов алгоритма не превысит C. Что очевидно неверно по указанным выше соображениям в случае теоретического алгоритма.Хорошо. Какая, по вашему, функция асимптотической сложности у алгоритма поиска в хэштаблице? И, сразу забегая, какая — у поиска простым перебором? И что выбрать?
Какая, по вашему, функция асимптотической сложности у алгоритма поиска в хэштаблице?
Уточнение. Та часть, что "не будет превышена", входит не в определение сложности, а в определение "O большого".
Существуют ещё символы Ω и Θ для оценки снизу и для точной оценки, и их тоже применяют для асимптотической сложности.
Почему разработчики Microsoft не разделили _entries на четыре массива, соответствующих полям Entry?
Чтобы оптимизировать локальность данных — снизить число кэш-миссов.
Некоторые аспекты работы с Dictionary при разработке нагруженных приложений