Aller au contenu

Optimiser les recherches dans les textes avec SearchValues<T>

Introduction

Il est fréquent de rechercher un ou plusieurs caractères dans une chaîne de caractères en utilisant les méthodes Contains(), IndexOf(), IndexOfAny(). Depuis la version 8 de .NET, une classe générique nommée SearchValues<T>, appartenant à l'espace de nom System.Buffers, permet d'optimiser ce type de recherche. En préparant une seule fois les valeurs recherchées, elle accélère les recherches répétées sans ajouter de complexité. Son utilisation est particulièrement pertinente dans les applications manipulant de grands volumes de données, comme les parseurs, les traitements de fichiers, l'analyse de flux réseau. Associée au type ReadOnlySpan<T> (présenté dans cet article), elle constitue une bonne pratique pour implémenter des algorithmes performants.

Utilisation de SearchValues<T>

Prenons un simple exemple qui consiste à rechercher le premier séparateur dans une chaîne de caractères. Lorsqu'une recherche est effectuée dans un traitement itératif et que le tableau est construit directement lors de l'appel de la méthode IndexOfAny, une nouvelle instance est créée à chaque itération :

foreach (string line in lines)
{
    int index = line.IndexOfAny([';', ',', '|', ':']);

    if (index >= 0)
    {
        // ...
    }
}

Si le traitement porte sur plusieurs milliers de chaînes de caractères, ces allocations répétées augmentent la pression sur le Garbage Collector et peuvent dégrader les performances lors de traitements intensifs. Même lorsque le tableau est initialisé en dehors du traitement itératif, chaque recherche doit comparer les caractères de la chaîne avec l'ensemble des caractères recherchés.

L'utilisation du type SearchValues<T> prépare une représentation interne optimisée de ces séparateurs, et permet d'accélérer ces comparaisons répétées :

private static readonly SearchValues<char> Separators =
    SearchValues.Create(";,|:");

La recherche peut ensuite être effectuée de manière très efficace, avec l'utilisation de la méthode AsSpan() :

int index = text.AsSpan().IndexOfAny(Separators);

SearchValues<T> n'a pas vocation à remplacer systématiquement IndexOfAny(char[]). Pour une recherche ponctuelle, son coût de création n'est généralement pas amorti.

Aussi, SearchValues<T> peut également être utilisé avec des octets. Il est ainsi particulièrement adapté à l’analyse de buffers, de contenus UTF-8, de fichiers binaires ou de flux réseau.

Méthodes compatibles avec SearchValues<T>

Voici quelques méthodes utilisées dans la recherche de caractères (` est de typeSearchValues`) :

// Recherche la première valeur appartenant à searchValues
span.IndexOfAny(searchValues);

// Recherche la dernière valeur appartenant à searchValues
span.LastIndexOfAny(searchValues);

// Indique si au moins une valeur appartient à searchValues
span.ContainsAny(searchValues);

// Indique si au moins une valeur n'appartient pas à searchValues
span.ContainsAnyExcept(searchValues);

// Recherche la première valeur n'appartenant pas à searchValues
span.IndexOfAnyExcept(searchValues);

Aussi, lorsque le processeur le permet, le runtime exploite les instructions SIMD (Single Instruction, Multiple Data), qui permettent de comparer plusieurs caractères simultanément au lieu de les traiter un par un. Cette vectorisation contribue à améliorer les performances sans nécessiter de code spécifique de la part du développeur.

Mesure des performances

Afin de comparer objectivement les deux approches, j'ai réalisé un benchmark avec BenchmarkDotNet sur une chaîne de 10 000 caractères, contenant le séparateur en dernière position, afin de comparer les méthodes :

  • Utilisation de la méthode IndexOfAny(char[]) : temps moyen de 449,2 nanosecondes

  • Utilisation de la méthode IndexOfAny(SearchValues<char>) : temps moyen de 255 nanosecondes

Ce benchmark montre que l'utilisation de SearchValues<char> permet de réduire le temps d'exécution d'environ 43 % dans ce scénario.

Recherche d'un mot dans un texte

Depuis la version 9 de .NET, SearchValues<T> a aussi été étendu pour prendre en charge la recherche répétée de plusieurs chaînes de caractères dans un texte, et pas uniquement de caractères, comme dans les exemples précédents. Voici un exemple :

private static readonly SearchValues<string> Keywords =
    SearchValues.Create(
        ["ERROR", "WARNING", "CRITICAL"],
        StringComparison.OrdinalIgnoreCase);

var log =
    """
    2026-07-28 10:15:42
    Warning: Connection lost with server.
    """;

var containsKeyword = log.AsSpan().ContainsAny(Keywords);

La variable containsKeyword contient la valeur true. En effet, le texte contenu dans la variable log contient le mot Warning, trouvé malgré la différence de casse grâce au paramètre StringComparison.OrdinalIgnoreCase.