В мире высоконагруженных .NET-приложений каждая наносекунда на счету. Когда ваш код обрабатывает миллионы запросов, даже микрооптимизации могут дать ощутимый прирост производительности. Две ключевые фичи, появившиеся в .NET 8 — SearchValues<T> и FrozenSet<T>/FrozenDictionary<TKey, TValue> — позволяют выжать максимум из «горячих путей» (hot paths) благодаря умной предварительной оптимизации.
Проблема: неоптимальный поиск
Представьте, что вам нужно проверить строку на наличие любого символа из набора (например, разделителей или спецсимволов). Стандартный string.IndexOfAny при каждом вызове анализирует набор символов и выбирает стратегию поиска. Для коротких строк это быстро, но в цикле или на больших объемах данных накладные расходы суммируются.
Решение:
SearchValues<T> — это immutable коллекция, создаваемая один раз для конкретного набора значений поиска. При создании экземпляра .NET один раз выполняет «тяжелую» работу:
Анализирует набор значений (один символ, диапазон, множество)
Выбирает оптимальный алгоритм под конкретное CPU (с учетом SIMD-инструкций)
Генерирует оптимизированные структуры данных для поиска
// Создаем оптимизированный набор один раз SearchValues<char> separators = SearchValues.Create(".,;!?"); // Используем многократно в горячем пути ReadOnlySpan<char> input = "Hello, world!"; int index = input.IndexOfAny(separators); // Мгновенный поиск
SearchValues<char> использует несколько стратегий в зависимости от входных данных:
Одиночный символ — максимально простая векторная проверка
Диапазон символов (например,
0-9A-Z) — битовая маска (bitmap) для O(1) проверкиНебольшой набор (≤4 символов) — специализированный SIMD-код
Большой набор — хеш-таблица с оптимизациями
Кроме того, SearchValues учитывает аппаратные возможности процессора (AVX2, SSE2), используя самые широкие доступные векторные регистры
Бенчмарки показывают впечатляющие результаты:
Сценарий | IndexOfAny | SearchValues | Ускорение |
|---|---|---|---|
Короткая строка (совпадение в начале) | ~5 нс | ~5 нс | 1x |
Длинная строка (2000 символов) | ~52 нс | ~4 нс | ~13x |
Для длинных строк (2062 символа) SearchValues работает в 13 раз быстрее стандартного подхода. Основной выигрыш достигается за счет того, что предварительный анализ выполняется только при создании коллекции, а не при каждом поиске.
Когда использовать SearchValues
Идеальные сценарии:
Поиск по одному и тому же набору значений миллионы раз
Обработка длинных строк (>100 символов)
Парсинг логов, токенизация, валидация входных данных
Неэффективно:
Единичный поиск
Очень короткие строки с совпадением в первой позиции
Набор значений постоянно меняется
FrozenSet и FrozenDictionary: константная скорость поиска
Проблема: HashSet/Dictionary
HashSet<T> и Dictionary<TKey, TValue> — отличные структуры, но они проектировались с учетом возможности изменений. Это накладывает ограничения:
Хеш-таблица должна поддерживать перехеширование
Структуры данных не могут быть полностью «выровнены» под конкретный набор ключей
При каждом вызове
Containsприходится выполнять дополнительные проверки
Решение:
FrozenSet<T> и FrozenDictionary<TKey, TValue> — неизменяемые коллекции из пространства имен System.Collections.Frozen, появившиеся в .NET 8. Они создаются один раз и больше никогда не меняются, что позволяет:
При создании — проанализировать все ключи и построить идеальную хеш-таблицу без коллизий (perfect hash function)
При поиске — выполнять минимальное количество операций, часто сводя проверку к одному обращению к массиву
// Создаем замороженный словарь из существующих данных var dict = Enumerable.Range(0, 100000) .ToFrozenDictionary(x => x, x => $"Value {x}"); // Поиск без аллокаций и с максимальной скоростью if (dict.TryGetValue(42, out string? value)) { Console.WriteLine(value); }
При создании FrozenSet/FrozenDictionary .NET выполняет следующие оптимизации:
Анализ распределения ключей — подбор хеш-функции, минимизирующей коллизии
Оптимальное выравнивание — данные размещаются в памяти для максимальной локальности (cache-friendly)
Удаление лишних проверок — поскольку коллекция неизменяема, многие рантайм-проверки становятся ненужными
В некоторых случаях для маленьких коллекций FrozenSet может использовать вообще битовую маску вместо хеш-таблицы.
Бенчмарки с 100 000 элементов показывают впечатляющие результаты:
Операция | Dictionary | HashSet | FrozenDictionary | FrozenSet |
|---|---|---|---|---|
| 3.78 нс | 4.47 нс | 1.87 нс | 3.37 нс |
Относительная скорость | 2.02x | 2.39x | 1x (базовый) | 1.8x |
FrozenDictionary.TryGetValue работает в 2 раза быстрее обычного Dictionary!
Когда использовать Frozen-коллекции
Идеальные сценарии:
Конфигурации, загруженные при старте (и больше не меняющиеся)
Статические lookup-таблицы (например, коды ошибок, статусы)
Данные, загруженные из базы при инициализации приложения
Read-heavy сценарии с тысячами операций чтения
Неэффективно:
Данные часто изменяются
Коллекция создается, используется пару раз и удаляется
Нужно добавлять/удалять элементы после создания
Практические рекомендации
Концепция «заплати сейчас, получи выгоду потом» (pay now, gain later) критически важна. Одноразовая инициализация на старте приложения окупается многократно в горячих путях.
public static class AppCache { // Создаем при старте приложения public static readonly SearchValues<char> UrlSafeChars = SearchValues.Create("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"); public static readonly FrozenSet<string> ValidCurrencies = new[] { "USD", "EUR", "GBP", "JPY", "CNY" }.ToFrozenSet(); }
Для максимальной производительности используйте оба инструмента там, где это уместно:
// Валидация email: быстрая проверка недопустимых символов SearchValues<char> invalidEmailChars = SearchValues.Create("()<>[]:;@\\,?\""); // Проверка доменов верхнего уровня (неизменяемый набор) FrozenSet<string> validTlds = LoadTldsFromConfig().ToFrozenSet();
Заключение
Всегда профилируйте конкретный сценарий с помощью BenchmarkDotNet. Иногда накладные расходы на создание коллекции могут превысить выгоду, если данных мало или поиск выполняется редко .
SearchValues<T> и FrozenCollections — примеры того, как умная предварительная оптимизация на уровне runtime может дать 2-10-кратный прирост производительности. Они особенно эффективны в read-heavy сценариях с повторяющимися операциями поиска.
Используйте SearchValues<T> для поиска символов и байт в строках и спанах, а FrozenSet<T>/FrozenDictionary<TKey, TValue> — для неизменяемых наборов объектов. Ваши горячие пути скажут вам спасибо.
