Comments 21
какой отвратительный дизайн, вот как об этой вещи должен узнать разработчик? Тут https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2?view=net-8.0 нет ни слова. У Рихтера тоже.
А большинству разработчиков эта фича ни разу не понадобится.
Спрятана же она потому, что возвращённые ссылки, фактически, инвалидируются при добавлении или удалении элементов из словаря, в то время как способа выразить это в API нету. Я бы тоже не стал держать в основном пронстранстве имён API, которое настолько легко использовать неправильно.
И вообще, нефиг делать такие большие структуры. 3-4 ссылочных поля - предел, за которым от структуры становится больше проблем чем пользы.
а какой тогда смысл делать "секретную" фичу, и не писать о ней в документации? Как разработчик, которому это потребовалось вообще должен вообще об этом узнать?
Ну автор же откуда-то про эту фичу знал? Теперь вот мы тоже знаем.
Я вот теперь тоже откуда-то узнал. Но почему функционал предназначенный для работы с диктами не упомянут в референсе на дикты? Имхо это провал архитектуры и документации.
Этот функционал предназначен для взаимодействия с кодом, выполняющимся не под управлением .NET. И находится там же, где всё подобное - в System.InteropServices.
В обычном (управляемом) коде .NET его лучше не использовать - как например и указатели, ровно по тем же причинам: unsafe.
Случайно обнаружил вот это issue https://github.com/dotnet/runtime/issues/27062, когда гуглил как структуры подружить со словарём. :)
Если мы получаем от словаря ссылку на структуру и потом работаем по ссылке, то чем, собственно говоря, это отличается от класса?
Да, есть небольшой выигрыш за счёт того, что ссылки не считаются и ансейф во все поля. Но этот выигрыш чувствителен только для сценария с большим количеством поисков и не пополняющимся словарём. Для такого сценария существуют контейнеры получше, вроде FrozenDictionary. А то и бинарное дерево.
И сравнение считаю некорректным. Структуры у вас с ружьём, направленным в ногу, а классы используют стандартный сейф механизм.
И ещё. Не нашёл в коде объявления GetHash и Equals. Может у вас хэш коллизит и поиск деградировал до линейного.
Так ключом в словаре является int, у него GetHashCode и Equals свои есть
Кто-то не верит, что там ключ int? Смотрим исходники: https://github.com/alexeyfv/speed-up-the-dictionary/blob/0f6ce393fe2fb7e32caef579640749e4dc2b18f2/CodeGen/SourceGenerator.cs#L235-L245
private readonly string _createDictionaryBenchmarkTemplate =
@" Dictionary<int, {0}{1}> _dict{0}{1} = new(Length);
{0}{1}[] _array{0}{1} = new {0}{1}[Length];
[Benchmark]
[BenchmarkCategory(""{1}"")]
public Dictionary<int, {0}{1}> {0}{1}DictCreate()
{{
_dict{0}{1} = new(Length);
return _dict{0}{1};
}}";
Если мы получаем от словаря ссылку на структуру и потом работаем по ссылке, то чем, собственно говоря, это отличается от класса?
В таком сценарии скорее всего все данные сразу попаду на кэш-линию, по-этому код будет работать быстрее.
чем, собственно говоря, это отличается от класса
Да, есть небольшой выигрыш за счёт того, что ссылки не считаются и ансейф во все поля
Вы сами ответили на вопрос. :)
Если рассматривать только то, что приведено в статье, то да, выигрыш небольшой. Если смотреть шире, как структуры влияют на всё приложение, то за счёт их использования можно ускорить работу и снизить потребление памяти. Данные в массиве data из примера же не берутся из воздуха. CLR в какой-то момент времени 100 000 аллоцировала память под 100 000 экземпляров классов + 1 раз для словаря. В случае со структурами, аллокация произошла только для словаря. Что быстрее: аллоцировать 100 001 раз память в куче или всего 1 раз? Но, опять же, всё зависит от сценария. Я не утверждаю, что рассмотренный мною способ единственно правильный.
По поводу FrozenDictionary
вы правы, работает быстрее.
| Method | Categories | Repeats | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
|----------------------------- |----------- |-------- |---------:|---------:|---------:|---------:|---------:|---------:|----------:|
| FrozenDictionary | Class | 100000 | 24.23 ms | 0.327 ms | 0.376 ms | 437.5000 | 437.5000 | 437.5000 | 7.29 MB |
| Dictionary | Struct | 100000 | 69.79 ms | 1.072 ms | 1.235 ms | 375.0000 | 375.0000 | 375.0000 | 4.14 MB |
| DictionaryCollectionsMarshal | Struct | 100000 | 34.22 ms | 2.077 ms | 2.392 ms | 428.5714 | 428.5714 | 428.5714 | 4.14 MB |
Опять же, выбор будет зависеть от сценария. Если создавать словарь нужно относительно редко (т.е. аллоцировать память для экземпляров классов), то это хорошее решение. Если же, например, на каждый запрос нужно достать из БД какие-то данные, преобразовать в словарь, потом осуществить поиск, то структуры + CollectionsMarshall, думаю, будут быстрее.
А точно ли со всей этой махиной "обрабатываем входящий запрос -> наполняем словарь -> лезем в БД через ORM" выигрыш в class->struct будет так сильно заметен по аллокациям?
Я давно видел эту штуку. Но никогда не проверял, что буде если:
ref var myRef = ref CollectionsMarshal.GetValueRefOrNullRef(dict, ket);
dict.Add(...); // тут происходит рост коллекции
myRef = value; // куда сейчас указывает myRef?
Либо на актуальную ячейку, либо в мусорный массив с копией старых данных, а куда точно - не определено.
Интереснее результат можно получить если удалить ключ из словаря.
Проглядел что вы об написали об этом. С одной стороны ожидаемо, но я в тайне надеялся что есть какая-то магия.
Интереснее результат можно получить если удалить ключ из словаря.
Причем, возможно - если удалить совсем другой ключ;-). Вы не посмотрели когда туда лазили, не открытая ли адресация там используется?
Ускоряем Dictionary в C# при помощи структур и CollectionsMarshal