SearchValues наконец-то демонстрирует ощутимое преимущество в .NET 10 при сканировании больших буферов на наличие редких разделителей: SearchValues справляется за 3,3 мс против 5,6 мс у IndexOfAny на полезной нагрузке объемом 32 МБ.

Почему появился SearchValues

.NET 8 представил SearchValues, чтобы предварительно вычислять набор символов (или байтов) и позволять среде выполнения выбирать самый быстрый алгоритм сканирования для этого набора. Создайте объект один раз, а затем используйте его повторно всякий раз, когда вам нужно найти любое из значений в span. Среда выполнения может использовать векторизованные инструкции, циклы без ветвлений или другие низкоуровневые приемы, с которыми рукописные альтернативы не могут сравниться без огромных усилий.

Важные бенчмарки

В тестах, вызвавших дискуссию, использовался 32-мегабайтный буфер, содержащий пять символов-разделителей. На .NET 10 были замерены три подхода:

  • IndexOfAny(char[]) – 10,2 мс
  • Ручной цикл foreach по span – 19,4 мс
  • SearchValues – 9,7 мс

SearchValues обошел встроенный IndexOfAny всего на полмиллисекунды. Результат не является тем самым драматичным «пятикратным ускорением», о котором пишут на форумах; современный .NET уже оптимизирует IndexOfAny для небольших наборов, сокращая разрыв.

Когда разделители встречаются редко — например, один раз на каждые 40 КБ вместо каждых 60 байт — картина меняется:

  • IndexOfAny(char[]) – 5,6 мс
  • SearchValues – 3,3 мс

Теперь SearchValues в 1,7 раза быстрее, потому что движок стремительно проходит через длинные последовательности несовпадающих данных с помощью векторизованных шагов, в то время как IndexOfAny переходит на менее агрессивный путь.

Скрытая цена

SearchValues не бесплатен. Создание объекта выделяет память и строит внутренние таблицы поиска. Если вы создаете его при каждом вызове метода, накладные расходы перекроют любую выгоду в производительности. Бенчмарк, вызывавший процедуру сканирования миллион раз на коротких строках, показал:

  • Статический (повторно используемый) SearchValues – всего 25,1 мс
  • SearchValues при каждом вызове – всего 70,2 мс

Создание объекта при каждом вызове делает всю операцию в три раза медленнее, чем если бы вы вообще не использовали SearchValues. Сохраняйте экземпляр в поле static readonly или используйте его повторно при вызовах.

Когда стоит использовать SearchValues

  • Большие входные данные, мало совпадений – Векторизованный путь раскрывается во всей красе, когда сканер может пропускать длинные участки, не встречая разделитель.
  • Повторные сканирования с тем же набором – Если одни и те же символы ищутся многократно, единоразовая настройка окупается.

Когда лучше остаться на IndexOfAny

  • Короткие строки или много совпадений – Дополнительные затраты на настройку перевешивают незначительный прирост скорости.
  • Код, который уже использует IndexOfAny – Реализация в современном .NET уже отлично оптимизирована для небольших наборов символов, поэтому переход на SearchValues может не дать заметного эффекта.

Распространенные ошибки

  • Создание объекта внутри горячих циклов – Это приводит к трехкратному замедлению, показанному выше.
  • Попытки «перехитрить» среду выполнения с помощью ручных циклов – Версия с ручным foreach оказалась в два раза медленнее обоих встроенных методов, что подтверждает: библиотеки .NET трудно превзойти без глубоких знаний инструкций SIMD.
  • Предположение об универсальном ускорении – Выигрыш в полмиллисекунды на плотных данных доказывает, что SearchValues — это не универсальный чит-код; его преимущества зависят от характера данных.

Итог

SearchValues дает явное преимущество только тогда, когда вы сканируете большие буферы на наличие редких символов и можете позволить себе разовые затраты на создание объекта. В большинстве повседневных сценариев поиска в строках — короткие входные данные, частые совпадения или код, который уже полагается на IndexOfAny — встроенный метод остается более простым и быстрым выбором. Используйте SearchValues избирательно, кэшируйте экземпляр, и вы избежите скрытых штрафов, получив реальный выигрыш в производительности.

Исходный код и полный набор тестов: https://dev.to/ssukhpinder/when-searchvalues-actually-pays-off-310l