web-dev-qa-db-fra.com

String.Substring () semble goulot d'étranglement ce code

Introduction

J'ai cet algorithme préféré que j'ai créé il y a quelque temps et que j'écris et réécris toujours dans de nouveaux langages de programmation, plateformes, etc. comme une sorte de référence. Bien que mon langage de programmation principal soit le C #, je viens de copier le code et de modifier légèrement la syntaxe, de le construire en Java et de l'exécuter 1000 fois plus rapidement.

Le code

Il y a pas mal de code mais je vais seulement présenter cet extrait qui semble être le problème principal:

for (int i = 0; i <= s1.Length; i++) 
{
    for (int j = i + 1; j <= s1.Length - i; j++)
    {
        string _s1 = s1.Substring(i, j);
        if (tree.hasLeaf(_s1))
         ...

Les données

Il est important de noter que la chaîne s1 dans ce test particulier a une longueur de 1 million de caractères (1 Mo).

Mesures

J'ai profilé l'exécution de mon code dans Visual Studio parce que je pensais que la manière dont je construisais mon arbre ou que je traversais n'était pas optimale. Après avoir examiné les résultats, il apparaît que la ligne string _s1 = s1.Substring(i, j); prend en charge plus de 90% du temps d'exécution!

Observations supplémentaires

Une autre différence que j'ai remarquée est que, bien que mon code soit à thread unique, Java parvient à l'exécuter à l'aide des 8 cœurs (utilisation du processeur à 100%) tout en utilisant les techniques Parallel.For () et multi-threading, mon C #. le code parvient à utiliser au maximum 35-40%. Étant donné que l'algorithme évolue de manière linéaire avec le nombre de cœurs (et la fréquence), j'ai compensé cela, mais l'extrait de code contenu dans Java exécute un ordre de grandeur plus rapide de 100 à 1000 fois.

Raisonnement

Je suppose que cela s’explique par le fait que les chaînes en C # sont immuables, donc String.Substring () doit créer une copie. le ramassage des ordures est en cours, cependant, je ne sais pas comment Substring est implémenté en Java.

Question

Quelles sont mes options à ce stade? Il n'y a aucun moyen de contourner le nombre et la longueur des sous-chaînes (ceci est déjà optimisé au maximum). Existe-t-il une méthode que je ne connais pas (ou une structure de données peut-être) qui pourrait résoudre ce problème pour moi?

Implémentation minimale requise (à partir de commentaires)

J'ai omis l'implémentation de l'arbre de suffixe qui est O(n) en construction et O(log(n)) en parcours.

public static double compute(string s1, string s2)
{
    double score = 0.00;
    suffixTree stree = new suffixTree(s2);
    for (int i = 0; i <= s1.Length; i++) 
    {
        int longest = 0;
        for (int j = i + 1; j <= s1.Length - i; j++)
        {
            string _s1 = s1.Substring(i, j);
            if (stree.has(_s1))
            {
                score += j - i;
                longest = j - i;
            }
            else break;
         };

        i += longest;
    };
    return score;
}

Extrait de capture d'écran du profileur

Notez que ceci a été testé avec la chaîne s1 d’une taille de 300 000 caractères. Pour une raison quelconque, 1 million de caractères ne finissent jamais en C #, alors que Java ne prend que 0,75 seconde. La mémoire utilisée et le nombre de corbeilles ne semblent pas indiquer un problème de mémoire. Le pic était d’environ 400 Mo, mais vu l’énorme arbre de suffixes, cela semble être normal. Pas de modèles étranges de collecte des ordures repérés non plus.

CPU profiler

Memory profiler

74
Ilhan

origine du problème

Après avoir eu une bataille glorieuse qui a duré deux jours et trois nuits (et des idées incroyables et des réflexions inspirées par les commentaires), j'ai finalement réussi à résoudre ce problème!

Je voudrais poster une réponse pour tous ceux qui rencontrent des problèmes similaires dans lesquels la fonction string.Substring(i, j) n'est pas une solution acceptable pour obtenir la sous-chaîne d'une chaîne, car celle-ci est trop longue et vous ne pouvez vous permettre la copie. par string.Substring(i, j) (il doit faire une copie car les chaînes C # sont immuables, aucun moyen de le contourner) ou la string.Substring(i, j) est appelée un nombre considérable de fois sur la même chaîne (comme dans mes boucles imbriquées for for ) donner du mal au éboueur, ou comme dans mon cas les deux!

tentatives

J'ai essayé de nombreuses suggestions telles que StringBuilder, Streams, allocation de mémoire non gérée à l'aide de Intptr et Marshal dans le bloc unsafe{} et même créer un IEnumerable et renvoyer les caractères par référence dans les positions données. Toutes ces tentatives ont finalement échoué, car une certaine forme de jonction des données a dû être effectuée car il n’était pas facile pour moi de parcourir mon arbre, caractère par caractère, sans compromettre les performances. Si seulement il y avait moyen de couvrir plusieurs adresses de mémoire dans un tableau à la fois, comme vous le feriez en C++ avec une arithmétique de pointeur .. sauf qu'il y a .. (crédits du commentaire de @Ivan Stoev)

La solution

La solution utilisait System.ReadOnlySpan<T> (ne pouvait pas être System.Span<T> en raison de la nature immuable des chaînes), ce qui permet, entre autres, de lire des sous-tableaux d'adresses mémoire dans un tableau existant sans en créer de copies.

Ce morceau de code posté:

string _s1 = s1.Substring(i, j);
if (stree.has(_s1))
{
    score += j - i;
    longest = j - i;
}

A été changé pour le suivant:

if (stree.has(i, j))
{
    score += j - i;
    longest = j - i;
}

stree.has() prend maintenant deux entiers (position et longueur de la chaîne) et fait:

ReadOnlySpan<char> substr = s1.AsSpan(i, j);

Notez que la variable substr est littéralement une référence à un sous-ensemble de caractères du tableau initial s1 et non à une copie! (La variable s1 a été rendue accessible à partir de cette fonction)

Notez qu’au moment de la rédaction de ce document, j’utilise C # 7.2 et .NET Framework 4.6.1, ce qui signifie que pour obtenir la fonctionnalité Span, je devais aller dans Projet> Gérer les paquets NuGet, cochez la case "Inclure la pré-version", puis recherchez Système. .Memory et installez-le.

En relançant le test initial (sur des chaînes de 1 million de caractères, soit 1 Mo), la vitesse est passée de 2 minutes et plus (j'ai abandonné l'attente après 2 minutes) à environ 86 millisecondes!

85
Ilhan