Skip to content

Repository files navigation

SimdProof

Код к статье про поиск по строке в .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):

Hit/Miss

Разрыв по длинам, лог-шкала:

Длины

Десктопы на AVX2 против серверов на AVX-512:

AVX2 vs AVX-512

Самописный Vector256/512 против BCL:

Vector512 таблица

Катастрофа на десктопе

Поиск гласной: перебор в цикле против IndexOfAny и SearchValues (лог-шкала):

SearchValues

Пруфы того, что AVX-512 медленнее AVX2 — не наша аномалия

  • 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

About

Бенчмарк поиска по строке в .NET: цикл против BCL, свой SIMD, AVX-512 vs AVX2. 4 машины, дизасм, воспроизводимо

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages