Код к статье про поиск по строке в .NET: ручной цикл против методов BCL против ручной векторизации. RyuJIT не векторизует произвольный цикл (issue #12466, с 2019), вся SIMD-скорость строк — внутри BCL. Плюс контринтуитив: серверный AVX-512 в этой задаче медленнее десктопного AVX2.
Статья на Хабре: Бенчмаркая поиск по строке: самописные циклы проигрывают от ×14 до ×154
- Реалистичные данные: случайный текст из букв и пробелов с фиксированным seed, а не строка из одного повторяющегося символа (на однородных данных ветвление предсказывается идеально и цифры врут). В алфавите нет 'z' — иначе регистронезависимый поиск 'Z' находит строчную 'z' в первых символах и меряет ранний выход вместо скана. Для
Vowel_*отдельный текст из согласных — по той же причине, в обычном тексте гласная попадается сразу. - Два сценария:
Hit— искомый символ в середине (возможен ранний выход),Miss— символа нет (полный скан). Виден и лучший, и худший случай для ручного цикла. - Несколько длин: 32 / 512 / 65536 — от коротких строк до буферов.
- Assert совпадения:
GlobalSetupпроверяет, что все методы возвращают одинаковый результат, иначе падает. Меряем одно и то же, а не разные ответы.
| Бенчмарк | Что | Ожидание |
|---|---|---|
IndexOf_Bcl (baseline) |
s.IndexOf(c) |
векторный, ymm/zmm под железо |
IndexOf_Manual |
ручной for |
скаляр, в разы медленнее |
IndexOf_Vector256 |
ручной AVX2 (ymm) | догоняет BCL, но не обгоняет |
IndexOf_Vector512 |
ручной AVX-512 (zmm) | «чемпион»: шире регистр — но по LLVM #91302 маски EVEX имеют бОльшую latency, может выйти медленнее ymm |
Count_Manual / Count_Bcl |
подсчёт символа: ручной цикл против span.Count(c) |
BCL векторный, в разы быстрее |
Equals_Manual / Equals_SequenceEqual |
сравнение равных строк: цикл против SequenceEqual |
BCL векторный, в разы быстрее |
Vowel_* |
поиск гласной: руками / IndexOfAny / SearchValues | SearchValues быстрее всех |
IgnoreCase_* |
регистронезависимо: ToLower в цикле против OrdinalIgnoreCase | разрыв ещё больше |
Свой цикл против IndexOf, четыре машины:
Ранний выход не спасает (Hit против Miss):
Разрыв по длинам, лог-шкала:
Десктопы на AVX2 против серверов на AVX-512:
Самописный Vector256/512 против BCL:
Поиск гласной: перебор в цикле против IndexOfAny и SearchValues (лог-шкала):
- LLVM #91302 — покомандный разбор: EVEX-маски
VPCMPEQBимеют latency 4 против 1 у AVX2, AVX2-код может быть в ~3 раза быстрее. - OpenVINO #11710 — Xeon с AVX-512 медленнее i7 с AVX2 на инференсе, подтверждено профилировщиком.
- Плюс даунклок: 512-битные инструкции снижают частоту ядра.
Бенчмарк: dotnet run -c Release -f net10.0. Прогон долгий (2 сценария × 3 длины × 13 методов × 3 рантайма). Результаты появятся в BenchmarkDotNet.Artifacts/results/.
Результаты моих прогонов на четырёх машинах — в Results/Comp_1..4 (№1 Ryzen 5950X, №2 i9-10900KF, №3 и №4 Xeon 4314), графики для статьи — в Results/Docs.
Дизасм: Disasm/snap.bat (Linux: bash snap.sh). В начале каждого txt — паспорт железа. Готовые листинги с машин №1 (Ryzen) и №3 (Xeon) лежат в Disasm/Listings_Ryzen_5950X/ и Disasm/Listings_Xeon_4314/. Ручной Vector256 — ymm, Vector512 — zmm (на Xeon), ручной цикл — скаляр.
Проверить полный AVX-512 в Vector: по умолчанию рантайм держит Vector<T> 256-битным даже на 512-битном железе. Прогнать с DOTNET_PreferredVectorBitWidth=512 — упрётся ли всё равно в частоту.
Subjects.cs— все методы, один источник на бенчмарк и дизасмBenchmarks/SimdSearchBench.cs— бенчмарк, Hit/Miss × длины × рантаймыDisasm/— снятие машинного кода с паспортом железаSETUP.md— установка SDK






