Понятно, что производительность поиска общего класса HashSet<T> выше, чем у общего класса List<T>. Просто сравните ключ на основе хеша с линейным подходом в классе List<T>.
Однако вычисление хеш-ключа само по себе может занять несколько циклов ЦП, поэтому для небольшого количества элементов линейный поиск может быть реальной альтернативой HashSet<T>.
Мой вопрос: где безубыточность?
Чтобы упростить сценарий (и быть справедливым), давайте предположим, что класс List<T> использует метод Equals() элемента для идентификации элемента.





По-разному. Если точный ответ действительно имеет значение, проведите профилирование и узнайте. Если вы уверены, что у вас никогда не будет больше определенного количества элементов в наборе, используйте список. Если число не ограничено, используйте HashSet.
Безубыточность будет зависеть от стоимости вычисления хэша. Вычисления хэшей могут быть тривиальными или нет ... :-) Всегда существует класс System.Collections.Specialized.HybridDictionary, который поможет вам не беспокоиться о точке безубыточности.
Вы также должны учитывать стоимость сравнения. В случае Contains (T) HashSet проведет сравнение, чтобы убедиться, что у него нет коллизии Hash, по сравнению со списком, выполняющим сравнение для каждого элемента, на который он смотрит, прежде чем он найдет правильный. Вы также должны учитывать распределение хэшей, сгенерированных T.GetHashCode (), как если бы это всегда возвращало то же значение, которое вы в основном заставляете HashSet делать то же самое, что и List.
Зависит от множества факторов ... Реализация списка, архитектура ЦП, JVM, семантика цикла, сложность метода равенства и т. д. К тому времени, когда список станет достаточно большим для эффективного тестирования (более 1000 элементов), двоичный файл на основе хеша Поисковые запросы превосходят линейный поиск, и разница только возрастает.
Надеюсь это поможет!
JVM ... или CLR :-)
Зависит от того, что вы хешируете. Если ваши ключи являются целыми числами, вам, вероятно, не понадобится очень много элементов, прежде чем HashSet станет быстрее. Если вы вводите его в строке, он будет медленнее и зависит от входной строки.
Конечно, вы могли бы довольно легко создать тест?
Ответ, как всегда, - «По-разному». Я предполагаю, что из тегов вы говорите о C#.
Лучше всего определить
и напишите несколько тестовых примеров.
Это также зависит от того, как вы сортируете список (если он вообще сортируется), какие сравнения необходимо провести, сколько времени занимает операция «Сравнить» для конкретного объекта в списке или даже от того, как вы собираетесь использовать коллекция.
Как правило, лучший вариант зависит не столько от размера данных, с которыми вы работаете, сколько от того, как вы собираетесь получить к ним доступ. У вас есть каждый фрагмент данных, связанный с определенной строкой или другими данными? Вероятно, лучше всего подойдет коллекция на основе хешей. Важен ли порядок данных, которые вы храните, или вам понадобится доступ ко всем данным одновременно? Тогда может быть лучше обычный список.
Дополнительный:
Конечно, в моих комментариях выше предполагается, что «производительность» означает доступ к данным. Еще кое-что, что нужно учесть: что вы ищете, когда говорите «производительность»? Подбирается ли индивидуальная ценность производительности? Это управление большими (10000, 100000 и более) наборами значений? Это производительность заполнения структуры данных данными? Удаление данных? Доступ к отдельным битам данных? Замена ценностей? Перебирать значения? Использование памяти? Скорость копирования данных? Например, если вы обращаетесь к данным с помощью строкового значения, но ваше основное требование к производительности - минимальное использование памяти, у вас могут возникнуть конфликтующие проблемы с дизайном.
Использование HashSet <> или List <> сводится к как вам нужно получить доступ к вашей коллекции. Если вам нужно гарантировать порядок элементов, используйте список. Если вы этого не сделаете, используйте HashSet. Пусть Microsoft позаботится о реализации своих алгоритмов и объектов хеширования.
HashSet будет обращаться к элементам без необходимости перечислять коллекцию (сложность О (1) или около нее), и поскольку List гарантирует порядок, в отличие от HashSet, некоторые элементы должны быть перечислены (сложность O (n)).
Список потенциально может вычислять смещение для конкретного элемента по его индексу (потому что все элементы одного типа и потенциально занимают один и тот же размер памяти). Таким образом, List не обязательно перечисляет его элементы
@ Lu55 - вопрос о поиск для элемента в коллекции. Типичный сценарий - это коллекция динамичный - элементы могли быть добавлены или удалены с момента последнего поиска данного элемента, поэтому показатель не имеет смысла (потому что он будет изменен). Если у вас есть коллекция статический (которая не изменится, пока вы выполняете свои вычисления), или элементы никогда не удаляются и всегда добавляются в конце, то List предпочтительнее, потому что вы можете запомнить индекс - это ситуация вы описываете.
Вы можете использовать SortedSet, если вам нужно отсортировать HashSet. Все еще намного быстрее, чем List.
Один фактор, который вы не принимаете во внимание, - это надежность функции GetHashcode (). С идеальной хэш-функцией HashSet явно будет иметь лучшую производительность поиска. Но по мере уменьшения хэш-функции будет уменьшаться и время поиска HashSet.
Вы неправильно смотрите на это. Да, линейный поиск по списку превзойдет HashSet для небольшого количества элементов. Но для таких небольших коллекций разница в производительности обычно не имеет значения. Обычно вам приходится беспокоиться о больших коллекциях, и именно здесь вы думать с точки зрения Big-O. Однако, если вы измерили реальное узкое место в производительности HashSet, вы можете попытаться создать гибридный List / HashSet, но вы сделаете это, проведя множество эмпирических тестов производительности, не задавая вопросов по SO.
when small collection becomes large enough to worry about HashSet vs List? tens, tens of thousands, billions of elements?
Нет, вы увидите значительную разницу в производительности для нескольких сотен элементов. Дело в том, что всегда используйте HashSet, если вы выполняете типы доступа, в которых хорош HashSet (например, элемент X в наборе). Если ваша коллекция настолько мала, что список работает быстрее, то эти поиски очень редко на самом деле являются узким местом в вашем приложении. Если вы можете измерить его как единицу, хорошо, вы можете попытаться его оптимизировать, но в противном случае вы зря теряете время.
Что делать, если у вас есть небольшая коллекция, которая повторяется много раз в цикле? Это не редкость.
@ om-nom-nom - я думаю, дело в том, что не имеет значения, где находится переломный момент, потому что: «Если производительность вызывает беспокойство, используйте HashSet<T>. В небольших случаях, когда List<T> может быть быстрее, разница несущественно ".
Вы можете использовать HybridDictionary, который автоматически определяет точку разрыва и принимает нулевые значения, что делает его практически таким же, как HashSet.
Проголосовали за эту идею, но никто, пожалуйста, никогда не использует это сегодня. Скажите нет не-дженерикам. Также словарь представляет собой сопоставление значений ключа, набор - нет.
Многие люди говорят, что как только вы достигнете размера, при котором скорость на самом деле является проблемой, HashSet<T> всегда будет побеждать List<T>, но это зависит от того, что вы делаете.
Допустим, у вас есть List<T>, в котором будет в среднем всего 5 элементов. При большом количестве циклов, если один элемент добавляется или удаляется в каждом цикле, вам может быть лучше использовать List<T>.
Я провел тест на своем компьютере, и, ну, он должен быть очень маленьким, чтобы получить преимущество от List<T>. Для списка коротких строк преимущество исчезло после размера 5, для объектов после размера 20.
1 item LIST strs time: 617ms
1 item HASHSET strs time: 1332ms
2 item LIST strs time: 781ms
2 item HASHSET strs time: 1354ms
3 item LIST strs time: 950ms
3 item HASHSET strs time: 1405ms
4 item LIST strs time: 1126ms
4 item HASHSET strs time: 1441ms
5 item LIST strs time: 1370ms
5 item HASHSET strs time: 1452ms
6 item LIST strs time: 1481ms
6 item HASHSET strs time: 1418ms
7 item LIST strs time: 1581ms
7 item HASHSET strs time: 1464ms
8 item LIST strs time: 1726ms
8 item HASHSET strs time: 1398ms
9 item LIST strs time: 1901ms
9 item HASHSET strs time: 1433ms
1 item LIST objs time: 614ms
1 item HASHSET objs time: 1993ms
4 item LIST objs time: 837ms
4 item HASHSET objs time: 1914ms
7 item LIST objs time: 1070ms
7 item HASHSET objs time: 1900ms
10 item LIST objs time: 1267ms
10 item HASHSET objs time: 1904ms
13 item LIST objs time: 1494ms
13 item HASHSET objs time: 1893ms
16 item LIST objs time: 1695ms
16 item HASHSET objs time: 1879ms
19 item LIST objs time: 1902ms
19 item HASHSET objs time: 1950ms
22 item LIST objs time: 2136ms
22 item HASHSET objs time: 1893ms
25 item LIST objs time: 2357ms
25 item HASHSET objs time: 1826ms
28 item LIST objs time: 2555ms
28 item HASHSET objs time: 1865ms
31 item LIST objs time: 2755ms
31 item HASHSET objs time: 1963ms
34 item LIST objs time: 3025ms
34 item HASHSET objs time: 1874ms
37 item LIST objs time: 3195ms
37 item HASHSET objs time: 1958ms
40 item LIST objs time: 3401ms
40 item HASHSET objs time: 1855ms
43 item LIST objs time: 3618ms
43 item HASHSET objs time: 1869ms
46 item LIST objs time: 3883ms
46 item HASHSET objs time: 2046ms
49 item LIST objs time: 4218ms
49 item HASHSET objs time: 1873ms
Вот эти данные, отображаемые в виде графика:

Вот код:
static void Main(string[] args)
{
int times = 10000000;
for (int listSize = 1; listSize < 10; listSize++)
{
List<string> list = new List<string>();
HashSet<string> hashset = new HashSet<string>();
for (int i = 0; i < listSize; i++)
{
list.Add("string" + i.ToString());
hashset.Add("string" + i.ToString());
}
Stopwatch timer = new Stopwatch();
timer.Start();
for (int i = 0; i < times; i++)
{
list.Remove("string0");
list.Add("string0");
}
timer.Stop();
Console.WriteLine(listSize.ToString() + " item LIST strs time: " + timer.ElapsedMilliseconds.ToString() + "ms");
timer = new Stopwatch();
timer.Start();
for (int i = 0; i < times; i++)
{
hashset.Remove("string0");
hashset.Add("string0");
}
timer.Stop();
Console.WriteLine(listSize.ToString() + " item HASHSET strs time: " + timer.ElapsedMilliseconds.ToString() + "ms");
Console.WriteLine();
}
for (int listSize = 1; listSize < 50; listSize+=3)
{
List<object> list = new List<object>();
HashSet<object> hashset = new HashSet<object>();
for (int i = 0; i < listSize; i++)
{
list.Add(new object());
hashset.Add(new object());
}
object objToAddRem = list[0];
Stopwatch timer = new Stopwatch();
timer.Start();
for (int i = 0; i < times; i++)
{
list.Remove(objToAddRem);
list.Add(objToAddRem);
}
timer.Stop();
Console.WriteLine(listSize.ToString() + " item LIST objs time: " + timer.ElapsedMilliseconds.ToString() + "ms");
timer = new Stopwatch();
timer.Start();
for (int i = 0; i < times; i++)
{
hashset.Remove(objToAddRem);
hashset.Add(objToAddRem);
}
timer.Stop();
Console.WriteLine(listSize.ToString() + " item HASHSET objs time: " + timer.ElapsedMilliseconds.ToString() + "ms");
Console.WriteLine();
}
Console.ReadLine();
}
Большое спасибо! Это отличное объяснение, я искал что-то, что могло бы добавлять и удалять быстрее, чем List<T> для игрового движка, и, поскольку у меня обычно будет большой объем объектов, такая коллекция будет идеальной.
На самом деле в .NET framework есть коллекция, которая переключается между списком и стабильной реализацией в зависимости от количества содержащихся в ней элементов: Гибридный словарь.
MS, кажется, отказался от этой мысли, поскольку у нее есть только не-универсальная версия.
Одна проблема, которую я вижу в этом тесте, заключается в том, что вы постепенно создаете оба списка и чередуете их. Это почти наверняка приводит к более разбросанному распределению, что отрицательно сказывается на производительности простого линейного поиска через List <>. Если вы можете получить массив / список и строки в непрерывный блок памяти, средство предварительной выборки ЦП поместит список (или его большие фрагменты) в свой кеш, и операция будет очень быстрой (до определенного момента, но должно быть намного выше 5 или 20!).
Каким бы полным ни был этот ответ, он не может ответить на исходный вопрос о производительности поиска по списку и хеш-сету. Вы тестируете, насколько быстро вы можете вставлять и удалять из них, что требует значительно больше времени и других характеристик производительности, чем поиск. Попробуйте еще раз, используя .Contains, и ваш график значительно изменится.
Есть ли причина, по которой вы добавляете и удаляете «string0» вместо «string» + i.ToString ())?
отличное изображение! что доказывает временную сложность;)
@ brandon-paddock Не могли бы вы объяснить, почему распределение распределений замедляет поиск по списку <>? Я думал, что скорость поиска в любой области памяти в ОЗУ одинакова, независимо от адреса.
@hypehuman ЦП не может работать напрямую с данными в системной памяти, а загружает данные из памяти в свой кеш для работы. Между запросом на перемещение памяти и фактическим поступлением памяти существует значительная задержка, поэтому ЦП часто запрашивает одновременное перемещение большего фрагмента непрерывной памяти. Идея заключается в том, что память, необходимая для следующей инструкции, вероятно, очень близка к памяти, используемой предыдущей инструкцией, и поэтому часто уже находится в кеше. Когда ваши данные разбросаны по всей памяти, шансы на удачу уменьшаются.
Посмотрите ответ @drzaus. Думаю, это более понятно и правильно.
@RobertMcKee Выполняя этот код с Contains и выполняя поиск каждого элемента в списке, включая создание HashSet (когда имеет смысл создавать HashSet для поиска из существующего List), я получаю безубыточность около 13 строк и 17 объектов. .
@ innominate227 Действительно отличный ответ. Но прошло 7 лет, есть ли интерес к аналогичным измерениям для .NET Core?
Просто подумал, что я подключусь к некоторым тестам для разных сценариев, чтобы проиллюстрировать предыдущие ответы:
И для каждого сценария поиск значений, которые появляются:
Перед каждым сценарием я генерировал списки случайных строк случайного размера, а затем загружал каждый список в хэш-набор. Каждый сценарий запускался 10 000 раз, в основном:
(тестовый псевдокод)
stopwatch.start
for X times
exists = list.Contains(lookup);
stopwatch.stop
stopwatch.start
for X times
exists = hashset.Contains(lookup);
stopwatch.stop
Протестировано на Windows 7, 12 ГБ ОЗУ, 64-разрядная версия, Xeon 2,8 ГГц
---------- Testing few small strings ------------
Sample items: (16 total)
vgnwaloqf diwfpxbv tdcdc grfch icsjwk
...
Benchmarks:
1: hashset: late -- 100.00 % -- [Elapsed: 0.0018398 sec]
2: hashset: middle -- 104.19 % -- [Elapsed: 0.0019169 sec]
3: hashset: end -- 108.21 % -- [Elapsed: 0.0019908 sec]
4: list: early -- 144.62 % -- [Elapsed: 0.0026607 sec]
5: hashset: start -- 174.32 % -- [Elapsed: 0.0032071 sec]
6: list: middle -- 187.72 % -- [Elapsed: 0.0034536 sec]
7: list: late -- 192.66 % -- [Elapsed: 0.0035446 sec]
8: list: end -- 215.42 % -- [Elapsed: 0.0039633 sec]
9: hashset: early -- 217.95 % -- [Elapsed: 0.0040098 sec]
10: list: start -- 576.55 % -- [Elapsed: 0.0106073 sec]
---------- Testing many small strings ------------
Sample items: (10346 total)
dmnowa yshtrxorj vthjk okrxegip vwpoltck
...
Benchmarks:
1: hashset: end -- 100.00 % -- [Elapsed: 0.0017443 sec]
2: hashset: late -- 102.91 % -- [Elapsed: 0.0017951 sec]
3: hashset: middle -- 106.23 % -- [Elapsed: 0.0018529 sec]
4: list: early -- 107.49 % -- [Elapsed: 0.0018749 sec]
5: list: start -- 126.23 % -- [Elapsed: 0.0022018 sec]
6: hashset: early -- 134.11 % -- [Elapsed: 0.0023393 sec]
7: hashset: start -- 372.09 % -- [Elapsed: 0.0064903 sec]
8: list: middle -- 48,593.79 % -- [Elapsed: 0.8476214 sec]
9: list: end -- 99,020.73 % -- [Elapsed: 1.7272186 sec]
10: list: late -- 99,089.36 % -- [Elapsed: 1.7284155 sec]
---------- Testing few long strings ------------
Sample items: (19 total)
hidfymjyjtffcjmlcaoivbylakmqgoiowbgxpyhnrreodxyleehkhsofjqenyrrtlphbcnvdrbqdvji...
...
Benchmarks:
1: list: early -- 100.00 % -- [Elapsed: 0.0018266 sec]
2: list: start -- 115.76 % -- [Elapsed: 0.0021144 sec]
3: list: middle -- 143.44 % -- [Elapsed: 0.0026201 sec]
4: list: late -- 190.05 % -- [Elapsed: 0.0034715 sec]
5: list: end -- 193.78 % -- [Elapsed: 0.0035395 sec]
6: hashset: early -- 215.00 % -- [Elapsed: 0.0039271 sec]
7: hashset: end -- 248.47 % -- [Elapsed: 0.0045386 sec]
8: hashset: start -- 298.04 % -- [Elapsed: 0.005444 sec]
9: hashset: middle -- 325.63 % -- [Elapsed: 0.005948 sec]
10: hashset: late -- 431.62 % -- [Elapsed: 0.0078839 sec]
---------- Testing many long strings ------------
Sample items: (5000 total)
yrpjccgxjbketcpmnvyqvghhlnjblhgimybdygumtijtrwaromwrajlsjhxoselbucqualmhbmwnvnpnm
...
Benchmarks:
1: list: early -- 100.00 % -- [Elapsed: 0.0016211 sec]
2: list: start -- 132.73 % -- [Elapsed: 0.0021517 sec]
3: hashset: start -- 231.26 % -- [Elapsed: 0.003749 sec]
4: hashset: end -- 368.74 % -- [Elapsed: 0.0059776 sec]
5: hashset: middle -- 385.50 % -- [Elapsed: 0.0062493 sec]
6: hashset: late -- 406.23 % -- [Elapsed: 0.0065854 sec]
7: hashset: early -- 421.34 % -- [Elapsed: 0.0068304 sec]
8: list: middle -- 18,619.12 % -- [Elapsed: 0.3018345 sec]
9: list: end -- 40,942.82 % -- [Elapsed: 0.663724 sec]
10: list: late -- 41,188.19 % -- [Elapsed: 0.6677017 sec]
---------- Testing few ints ------------
Sample items: (16 total)
7266092 60668895 159021363 216428460 28007724
...
Benchmarks:
1: hashset: early -- 100.00 % -- [Elapsed: 0.0016211 sec]
2: hashset: end -- 100.45 % -- [Elapsed: 0.0016284 sec]
3: list: early -- 101.83 % -- [Elapsed: 0.0016507 sec]
4: hashset: late -- 108.95 % -- [Elapsed: 0.0017662 sec]
5: hashset: middle -- 112.29 % -- [Elapsed: 0.0018204 sec]
6: hashset: start -- 120.33 % -- [Elapsed: 0.0019506 sec]
7: list: late -- 134.45 % -- [Elapsed: 0.0021795 sec]
8: list: start -- 136.43 % -- [Elapsed: 0.0022117 sec]
9: list: end -- 169.77 % -- [Elapsed: 0.0027522 sec]
10: list: middle -- 237.94 % -- [Elapsed: 0.0038573 sec]
---------- Testing many ints ------------
Sample items: (10357 total)
370826556 569127161 101235820 792075135 270823009
...
Benchmarks:
1: list: early -- 100.00 % -- [Elapsed: 0.0015132 sec]
2: hashset: end -- 101.79 % -- [Elapsed: 0.0015403 sec]
3: hashset: early -- 102.08 % -- [Elapsed: 0.0015446 sec]
4: hashset: middle -- 103.21 % -- [Elapsed: 0.0015618 sec]
5: hashset: late -- 104.26 % -- [Elapsed: 0.0015776 sec]
6: list: start -- 126.78 % -- [Elapsed: 0.0019184 sec]
7: hashset: start -- 130.91 % -- [Elapsed: 0.0019809 sec]
8: list: middle -- 16,497.89 % -- [Elapsed: 0.2496461 sec]
9: list: end -- 32,715.52 % -- [Elapsed: 0.4950512 sec]
10: list: late -- 33,698.87 % -- [Elapsed: 0.5099313 sec]
Интересно. Спасибо, что запустили это. К сожалению, я подозреваю, что эти обсуждения вызывают ненужный рефакторинг. Надеюсь, что вывод для большинства людей состоит в том, что в вашем наихудшем сценарии List по-прежнему требует всего 0,17 миллисекунды для выполнения одного поиска и вряд ли потребует замены HashSet, пока частота поиска не достигнет абсурдного уровня. К тому времени использование List обычно становится наименьшей из проблем.
На данный момент это не актуальная информация .. Или, может быть, изначально неверно ... Я только что проверил небольшие значения от 2 до 8 символов. List / HashSet создавались для каждых 10 значений ... HashSet медленнее на 30% ... Если используется емкость в List, то разница даже ~ 40%. HashSet становится быстрее на 10%, только если у List нет указанной емкости и проверяется каждое значение перед добавлением всего списка.
Если количество предметов уменьшено до 4, то Список снова побеждает даже в худшем сценарии (с разницей в 10%). Поэтому я не рекомендую использовать HashSet для небольшого набора строк (скажем, <20). И это то, что отличается от ваших «нескольких маленьких» тестов.
@Maxim не может сказать, что мои результаты "неправильные" - это то, что произошло на моей машине. YMMV. Фактически, я просто запустил их снова (gist.github.com/zaus/014ac9b5a78b267aa1643d63d30c7554) на новом твердотельном компьютере Win10 4,0 ГГц 16 ГБ и получил аналогичные результаты. Вывод, который я вижу, заключается в том, что производительность хеш-набора была более стабильной, независимо от того, где был ключ поиска и насколько велик список, в то время как производительность списка сильно варьировалась от лучшей до более чем в 300 раз медленнее. Но, как сначала прокомментировал Пол Уоллс, мы говорим о серьезной микрооптимизации.
@Maxim для справки: dotnetfiddle.net/5taRDd - не стесняйтесь экспериментировать.
@drzaus Ответил по сути.
Что-то очень странное произошло с регистром начала списка нескольких коротких строк, это не может быть правдой.
По сути, бессмысленно сравнивать две структуры для представление, которые ведут себя по-разному. Используйте структуру, которая передает намерение. Даже если вы говорите, что ваш List<T> не будет иметь дубликатов и порядок итераций не имеет значения, что делает его сопоставимым с HashSet<T>, использование List<T> все равно будет плохим выбором, поскольку он относительно менее устойчив к сбоям.
Тем не менее, я проверю производительность некоторые другие аспекты,
+------------+--------+-------------+-----------+----------+----------+-----------+
| Collection | Random | Containment | Insertion | Addition | Removal | Memory |
| | access | | | | | |
+------------+--------+-------------+-----------+----------+----------+-----------+
| List<T> | O(1) | O(n) | O(n) | O(1)* | O(n) | Lesser |
| HashSet<T> | O(n) | O(1) | n/a | O(1) | O(1) | Greater** |
+------------+--------+-------------+-----------+----------+----------+-----------+
Несмотря на то, что в обоих случаях сложение составляет O (1), оно будет относительно медленнее в HashSet, поскольку требует затрат на предварительное вычисление хэш-кода перед его сохранением.
Превосходная масштабируемость HashSet требует затрат памяти. Каждая запись сохраняется как новый объект вместе со своим хэш-кодом. эта статья может дать вам представление.
Мой вопрос (шесть лет назад) не касался производительности теоретический.
HashSet разрешает произвольный доступ с помощью ElementAt (), и я думаю, что это будет время O (n). Кроме того, возможно, вы могли бы указать в своей таблице, допускает ли каждая коллекция дубликаты (например, списки делают, а хеш-наборы нет).
@DanW в таблице сравниваю чисто производительность, а не поведенческие характеристики. Спасибо за подсказку ElementAt.
ElementAt - это просто расширение LINQ ... оно не делает ничего, что вы не можете сделать, и оптимизирует лучше другим методом, который вы добавляете сами. Я думаю, что таблица имеет больше смысла без учета ElementAt, поскольку все другие методы явно существуют в этих классах.
Спасибо за эту таблицу, в моем случае использования мне нужно добавлять и удалять цели в заполненную коллекцию каждый раз, когда они включаются / отключаются, и это помогло мне сделать правильный выбор (HashSet).
Если вы действительно хотите минимизировать время поиска, подумайте также о массивах и отсортированных массивах. Чтобы правильно ответить на этот вопрос, необходим тест, но вам нужно рассказать нам больше о T. Кроме того, на производительность HashSet может влиять время работы T.GetHashCode ().