SearchValues<T> mostra finalmente un vantaggio misurabile in .NET 10 quando si scansionano grandi buffer alla ricerca di delimitatori sparsi, con SearchValues che completa l'operazione in 3,3 ms rispetto ai 5,6 ms di un'esecuzione di IndexOfAny su un payload di 32 MB.

Perché è apparso SearchValues

.NET 8 ha introdotto SearchValues<T> per pre-calcolare un set di caratteri (o byte) e consentire al runtime di scegliere l'algoritmo di scansione più veloce per quel set. Crea l'oggetto una sola volta, poi riutilizzalo ogni volta che devi individuare uno dei valori in uno span. Il runtime può impiegare istruzioni vettorializzate, loop senza branching o altri trucchi di basso livello che le alternative scritte a mano non possono eguagliare senza un grande sforzo.

Benchmark che contano

I test che hanno scatenato la discussione hanno utilizzato un buffer da 32 MB contenente cinque caratteri delimitatori. Tre approcci sono stati cronometrati su .NET 10:

  • IndexOfAny(char[]) – 10,2 ms
  • Ciclo foreach manuale sullo span – 19,4 ms
  • SearchValues – 9,7 ms

SearchValues ha superato l'integrato IndexOfAny di soli mezzo millisecondo. Il risultato non è la clamorosa affermazione "cinque volte più veloce" che circola sui forum; il .NET moderno ottimizza già IndexOfAny per set di piccole dimensioni, riducendo il divario.

Quando i delimitatori sono rari — compaiono una volta ogni 40 KB invece che ogni 60 byte — il quadro cambia:

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

Ora SearchValues è 1,7 volte più veloce perché il motore attraversa rapidamente lunghe sequenze di dati non corrispondenti con passaggi vettorializzati, mentre IndexOfAny ricorre a un percorso meno aggressivo.

Il costo nascosto

SearchValues non è gratuito. La costruzione dell'oggetto alloca memoria e crea tabelle di ricerca interne. Se lo istanzi ad ogni chiamata di metodo, l'overhead annulla qualsiasi vantaggio in fase di runtime. Un benchmark che ha chiamato la routine di scansione un milione di volte su righe brevi ha mostrato:

  • SearchValues statico (riutilizzato) – 25,1 ms totali
  • SearchValues per chiamata – 70,2 ms totali

Creare l'oggetto per ogni chiamata rende l'intera operazione tre volte più lenta rispetto al non usare affatto SearchValues. Memorizza l'istanza in un campo static readonly o riutilizzala in altro modo tra le chiamate.

Quando ricorrere a SearchValues

  • Input di grandi dimensioni, pochi match – Il percorso vettorializzato eccelle quando lo scanner può saltare lunghi tratti senza incontrare un delimitatore.
  • Scansioni ripetute con lo stesso set – Se gli stessi caratteri vengono cercati molte volte, la configurazione iniziale una tantum ripaga l'investimento.

Quando restare con IndexOfAny

  • Stringhe brevi o molti match – Il costo di configurazione extra supera il marginale guadagno di velocità.
  • Codice che utilizza già IndexOfAny – L'implementazione del .NET moderno è già altamente ottimizzata per set di caratteri ridotti, quindi sostituirla con SearchValues potrebbe non portare benefici significativi.

Errori comuni

  • Inserire la costruzione all'interno di loop critici (hot loops) – Porta al rallentamento di tre volte mostrato sopra.
  • Cercare di "essere più furbi" del runtime con loop manuali – La versione manuale con foreach è stata due volte più lenta di entrambi i metodi integrati, confermando che le librerie .NET sono difficili da battere senza una profonda conoscenza delle istruzioni SIMD.
  • Presumere accelerazioni universali – Il vantaggio di mezzo millisecondo su dati densi dimostra che SearchValues non è un codice truccato universale; i suoi benefici dipendono dai dati.

Conclusione

SearchValues<T> offre un vantaggio chiaro solo quando si scansionano grandi buffer alla ricerca di caratteri poco frequenti e ci si può permettere il costo di costruzione iniziale. Per la maggior parte degli scenari quotidiani di ricerca nelle stringhe — input brevi, match frequenti o codice che si affida già a IndexOfAny — il metodo integrato rimane la scelta più semplice e veloce. Usa SearchValues in modo selettivo, metti in cache l'istanza e eviterai la penalità nascosta, ottenendo al contempo il reale vantaggio in termini di prestazioni.

Codice sorgente e suite di test completa: https://dev.to/ssukhpinder/when-searchvalues-actually-pays-off-310l