SearchValues<T> w końcu wykazuje mierzalną przewagę w .NET 10 podczas skanowania dużych buforów w poszukiwaniu rzadkich ograniczników, osiągając czas 3,3 ms w porównaniu do 5,6 ms dla IndexOfAny przy 32 MB danych.
Dlaczego pojawiło się SearchValues
.NET 8 wprowadziło SearchValues<T>, aby wstępnie obliczyć zestaw znaków (lub bajtów) i pozwolić środowisku uruchomieniowemu (runtime) wybrać najszybszy algorytm skanowania dla tego zestawu. Tworzysz obiekt raz, a następnie używasz go wielokrotnie, gdy musisz zlokalizować dowolną z wartości w span. Środowisko uruchomieniowe może wykorzystywać instrukcje wektoryzowane, pętle bez rozgałęzień (branch-free loops) lub inne niskopoziomowe triki, których ręcznie pisane alternatywy nie są w stanie dorównać bez dużego nakładu pracy.
Benchmarki, które mają znaczenie
Testy, które wywołały dyskusję, wykorzystały 32 MB bufor zawierający pięć znaków ograniczników. Na .NET 10 zmierzono czas trzech podejść:
- IndexOfAny(char[]) – 10,2 ms
- Manualna pętla foreach nad spanem – 19,4 ms
- SearchValues
– 9,7 ms
SearchValues pokonało wbudowane IndexOfAny o zaledwie pół milisekundy. Wynik ten nie potwierdza sensacyjnych twierdzeń o „pięciokrotnie większej prędkości”, które krążą na forach; nowoczesne .NET optymalizuje już IndexOfAny dla małych zestawów, zmniejszając tę różnicę.
Gdy ograniczniki występują rzadko — np. raz na 40 KB zamiast co 60 bajtów — sytuacja się zmienia:
- IndexOfAny(char[]) – 5,6 ms
- SearchValues
– 3,3 ms
Teraz SearchValues jest 1,7 × szybsze, ponieważ silnik błyskawicznie przechodzi przez długie ciągi danych niepasujących za pomocą kroków wektoryzowanych, podczas gdy IndexOfAny przechodzi na mniej agresywną ścieżkę.
Ukryty koszt
SearchValues nie jest darmowe. Konstruowanie obiektu alokuje pamięć i buduje wewnętrzne tablice wyszukiwania (lookup tables). Jeśli będziesz go instancjonować przy każdym wywołaniu metody, narzut (overhead) przyćmi wszelkie zyski wydajnościowe. Benchmark, który wywoływał procedurę skanowania milion razy na krótkich liniach, wykazał:
- Static (reused) SearchValues – 25,1 ms łącznie
- Per-call SearchValues – 70,2 ms łącznie
Tworzenie obiektu przy każdym wywołaniu sprawia, że cała operacja jest trzykrotnie wolniejsza niż w przypadku całkowitego zrezygnowania z SearchValues. Przechowuj instancję w polu static readonly lub w inny sposób używaj jej wielokrotnie w kolejnych wywołaniach.
Kiedy sięgać po SearchValues
- Duże dane wejściowe, mało dopasowań – Ścieżka wektoryzowana błyszczy, gdy skaner może przeskakiwać przez długie odcinki bez napotkania ogranicznika.
- Powtarzane skanowanie tego samego zestawu – Jeśli te same znaki są przeszukiwane wielokrotnie, jednorazowe przygotowanie się opłaca.
Kiedy pozostać przy IndexOfAny
- Krótkie ciągi znaków lub wiele dopasowań – Dodatkowy koszt przygotowania przewyższa marginalny zysk prędkości.
- Kod, który już używa IndexOfAny – Implementacja w nowoczesnym .NET jest już bardzo dobrze zoptymalizowana pod kątem małych zestawów znaków, więc zamiana na
SearchValuesmoże nie przynieść istotnej zmiany.
Typowe pułapki
- Umieszczanie konstrukcji w gorących pętlach (hot loops) – Prowadzi do trzykrotnego spowolnienia opisanego powyżej.
- Próby „przechytrzenia” środowiska uruchomieniowego za pomocą ręcznych pętli – Wersja z ręcznym
foreachbyła dwukrotnie wolniejsza od obu wbudowanych metod, co potwierdza, że biblioteki .NET trudno przebić bez głębokiej wiedzy o instrukcjach SIMD. - Zakładanie uniwersalnego przyspieszenia – Zwycięstwo o pół milisekundy na gęstych danych dowodzi, że
SearchValuesnie jest uniwersalnym „cheat codem”; jego korzyści zależą od rodzaju danych.
Podsumowanie
SearchValues<T> daje wyraźną przewagę tylko wtedy, gdy skanujesz duże bufory w poszukiwaniu rzadko występujących znaków i możesz sobie pozwolić na jednorazowy koszt konstrukcji. W większości codziennych scenariuszy wyszukiwania w ciągach znaków — krótkie dane wejściowe, częste dopasowania lub kod, który już polega na IndexOfAny — wbudowana metoda pozostaje prostszym i szybszym wyborem. Używaj SearchValues selektywnie, buforuj instancję, a unikniesz ukrytej kary wydajnościowej, czerpiąc jednocześnie realne korzyści z szybkości.
Kod źródłowy i pełny zestaw testów: https://dev.to/ssukhpinder/when-searchvalues-actually-pays-off-310l
