web-dev-qa-db-fra.com

Le moyen le plus rapide de remplacer plusieurs chaînes dans une chaîne énorme

Je cherche le moyen le plus rapide de remplacer plusieurs (~ 500) sous-chaînes d’une grande chaîne (~ 1 Mo). Quoi que j'ai essayé, il semble que String.Replace soit le moyen le plus rapide de le faire.

Je me soucie juste de la manière la plus rapide possible. Pas la lisibilité du code, la maintenabilité, etc. Je ne me soucie pas si j'ai besoin d'utiliser un code non sécurisé ou de prétraiter la chaîne d'origine.

EDIT: Après les commentaires, j'ai ajouté quelques détails supplémentaires:

Chaque itération de remplacement remplacera ABC sur la chaîne par une autre chaîne (différente par itération de remplacement). La chaîne à remplacer sera TOUJOURS la même - ABC sera toujours ABC. Jamais ABD. Donc, s'il y a 400.000, des milliers remplacent les itérations. La même chaîne - ABC - sera remplacée par une autre chaîne (différente) à chaque fois. 

Je peux être en contrôle sur ce que ABC est. Je peux le rendre super court ou super long tant que cela n'affecte pas les résultats. Clairement, ABC ne peut pas être hello car hello existera en tant que mot dans la plupart des chaînes en entrée.

Exemple de saisie: ABCDABCABCDABCABCDABCABCDABCD

Exemple de remplacement de chaîne: BC

Exemple remplacer par des chaînes: AA, BB, CC, DD, EE (5 iterations)

Exemple de sorties:

AAADAAAAAADAAAAAADAAAAAADAAAD
ABBDABBABBDABBABBDABBABBDABBD
ACCDACCACCDACCACCDACCACCDACCD
ADDDADDADDDADDADDDADDADDDADDD
AEEDAEEAEEDAEEAEEDAEEAEEDAEED

Cas moyen: La chaîne en entrée est comprise entre 100 et 200 Ko avec 40 000 remplacements d'itérations . Dans le pire des cas: la chaîne en entrée est de 1-2 Mo avec 400 000 remplacements d'itérations.

Je peux tout faire. Faites-le en parallèle, ne le faites pas en sécurité, etc. Peu importe comment je le fais. Ce qui compte, c’est qu’il soit aussi rapide que possible.

Merci

26
Yannis

Comme je n’étais que légèrement intéressé par ce problème, j’ai élaboré peu de solutions. Avec les optimisations hardcore, il est possible de descendre encore plus.

Pour obtenir la dernière source: https://github.com/ChrisEelmaa/StackOverflow/blob/master/FastReplacer.cs

Et la sortie

----------------------------------------------- --------
 Mise en œuvre | Moyenne | Sessions séparées | 
 | ---------------------- + --------- + ---------- ---------- | 
 | Simple | 3485 | 9002, 4497, 443, 0 | 
 | SimpleParallel | 1298 | 3440, 1606, 146, 0 | 
 | ParallelSubstring | 470 | 1259, 558, 64, 0 | 
 | Fredou dangereux | 356 | 953, 431, 41, 0 | 
 | Unsafe + unmanaged_mem | 92 | 229, 114, 18, 8 | 
------------------------------------------- ----------------

Vous ne battrez probablement pas les gars .NET en élaborant votre propre méthode de remplacement, il est très probablement déjà dangereux. Je crois que vous pouvez le réduire d'un facteur deux si vous l'écrivez complètement en C.

Mes implémentations peuvent être buggées, mais vous pouvez avoir une idée générale.

24
Erti-Chris Eelmaa

Utiliser unsafe et compiler en tant que x64

résultat:

Implementation       | Exec   | GC
#1 Simple            | 4706ms |  0ms
#2 Simple parallel   | 2265ms |  0ms
#3 ParallelSubstring |  800ms | 21ms
#4 Fredou unsafe     |  432ms | 15ms

prenez le code de Erti-Chris Eelmaa et remplacez mon précédent par celui-ci.

Je ne pense pas que je ferai une autre itération mais j'ai appris quelques trucs avec unsafe, ce qui est une bonne chose :-)

    private unsafe static void FredouImplementation(string input, int inputLength, string replace, string[] replaceBy)
    {
        var indexes = new List<int>();

        //input = "ABCDABCABCDABCABCDABCABCDABCD";
        //inputLength = input.Length;
        //replaceBy = new string[] { "AA", "BB", "CC", "DD", "EE" };

        //my own string.indexof to save a few ms
        int len = inputLength;

        fixed (char* i = input, r = replace)
        {
            int replaceValAsInt = *((int*)r);

            while (--len > -1)
            {
                if (replaceValAsInt == *((int*)&i[len]))
                {
                    indexes.Add(len--);
                }
            }                
        }

        var idx = indexes.ToArray();
        len = indexes.Count;

        Parallel.For(0, replaceBy.Length, l =>
            Process(input, inputLength, replaceBy[l], idx, len)
        );
    }

    private unsafe static void Process(string input, int len, string replaceBy, int[] idx, int idxLen)
    {
        var output = new char[len];

        fixed (char* o = output, i = input, r = replaceBy)
        {
            int replaceByValAsInt = *((int*)r);

            //direct copy, simulate string.copy
            while (--len > -1)
            {
                o[len] = i[len];
            }

            while (--idxLen > -1)
            {
                ((int*)&o[idx[idxLen]])[0] = replaceByValAsInt;
            }
        }

        //Console.WriteLine(output);
    }
4
Fredou

On dirait que vous marquez la chaîne de caractères?

Comme exemple naïf, vous pouvez utiliser la génération de code pour rendre la méthode suivante

public string Produce(string tokenValue){

    var builder = new StringBuilder();
    builder.Append("A");
    builder.Append(tokenValue);
    builder.Append("D");

    return builder.ToString();

}

Si vous exécutez les itérations suffisamment de fois, le temps nécessaire pour construire le modèle sera rentable. Vous pouvez alors également appeler cette méthode en parallèle, sans aucun effet secondaire .

1
Adam Mills

J'ai fait une variante du code de Fredou qui nécessite moins de comparaisons car cela fonctionne sur int* au lieu de char*. Il faut toujours n itérations pour une chaîne de n longueur, il faut juste faire moins de comparaison. Vous pouvez avoir des itérations n/2 si la chaîne est parfaitement alignée sur 2 (la chaîne à remplacer ne peut donc apparaître qu'aux index 0, 2, 4, 6, 8, etc.) ou même n/4 si elle est alignée sur 4 (vous utiliseriez long* ). Je ne suis pas très doué pour bidouiller comme ça, alors quelqu'un pourrait peut-être trouver un défaut évident dans mon code qui pourrait être plus efficace. J'ai vérifié que le résultat de ma variation est le même que celui du simple string.Replace.

De plus, je pense que le code 500x string.Copy pourrait être amélioré, mais je ne l'ai pas encore examiné.

Mes résultats (Fredou II):

IMPLEMENTATION       |  EXEC MS | GC MS
#1 Simple            |     6816 |     0
#2 Simple parallel   |     4202 |     0
#3 ParallelSubstring |    27839 |     4
#4 Fredou I          |     2103 |   106
#5 Fredou II         |     1334 |    91

Donc, environ 2/3 du temps (x86, mais x64 était à peu près le même).

Pour ce code:

private unsafe struct TwoCharStringChunk
{
  public fixed char chars[2];
}

private unsafe static void FredouImplementation_Variation1(string input, int inputLength, string replace, TwoCharStringChunk[] replaceBy)
{
  var output = new string[replaceBy.Length];

  for (var i = 0; i < replaceBy.Length; ++i)
    output[i] = string.Copy(input);

  var r = new TwoCharStringChunk();
  r.chars[0] = replace[0];
  r.chars[1] = replace[1];

  _staticToReplace = r;

  Parallel.For(0, replaceBy.Length, l => Process_Variation1(output[l], input, inputLength, replaceBy[l]));
}

private static TwoCharStringChunk _staticToReplace ;

private static unsafe void Process_Variation1(string output, string input, int len, TwoCharStringChunk replaceBy)
{
  int n = 0;
  int m = len - 1;

  fixed (char* i = input, o = output, chars = _staticToReplace .chars)
  {
    var replaceValAsInt = *((int*)chars);
    var replaceByValAsInt = *((int*)replaceBy.chars);

    while (n < m)
    {
      var compareInput = *((int*)&i[n]);

      if (compareInput == replaceValAsInt)
      {
        ((int*)&o[n])[0] = replaceByValAsInt;
        n += 2;
      }
      else
      {
        ++n;
      }
    }
  }
}

La structure avec le tampon fixe n'est pas strictement nécessaire ici et aurait pu être remplacée par un simple champ int, mais développez le char[2] en char[3] et ce code peut également être utilisé avec des chaînes de trois lettres, ce qui ne serait pas possible si c'était un champ int.

Il a également fallu apporter quelques modifications à Program.cs, alors voici le résumé complet:

https://Gist.github.com/JulianR/7763857

EDIT: Je ne suis pas sûr de savoir pourquoi mon ParallelSubstring est si lent. J'exécute .NET 4 en mode publication, pas de débogueur, ni x86 ni x64.

1
JulianR

J'ai eu un problème similaire sur un projet et j'ai implémenté une solution Regex pour effectuer plusieurs remplacements et insensibles à la casse sur un fichier.

À des fins d'efficacité, j'ai défini des critères pour passer par la chaîne d'origine une seule fois.

J'ai publié une application console simple pour tester certaines stratégies sur https://github.com/nmcc/Spikes/tree/master/StringMultipleReplacements

Le code de la solution Regex est similaire à ceci:

Dictionary<string, string> replacements = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
    // Fill the dictionary with the proper replacements:

        StringBuilder patternBuilder = new StringBuilder();
                patternBuilder.Append('(');

                bool firstReplacement = true;

                foreach (var replacement in replacements.Keys)
                {
                    if (!firstReplacement)
                        patternBuilder.Append('|');
                    else
                        firstReplacement = false;

                    patternBuilder.Append('(');
                    patternBuilder.Append(Regex.Escape(replacement));
                    patternBuilder.Append(')');
                }
                patternBuilder.Append(')');

                var regex = new Regex(patternBuilder.ToString(), RegexOptions.IgnoreCase);

                return regex.Replace(sourceContent, new MatchEvaluator(match => replacements[match.Groups[1].Value]));

EDIT: Les temps d'exécution de l'application de test sur mon ordinateur sont les suivants:

  • En boucle à travers l'appel de remplacement. String.Substring () (CASE SENSITIVE): 2ms
  • Passage unique en utilisant Regex avec plusieurs remplacements à la fois (insensible à la casse): 8 ms
  • Boucle dans les remplacements à l’aide de ReplaceIgnoreCase Extension (insensible à la casse): 55 ms
0
nunoc

Étant donné que votre chaîne d'entrée peut atteindre 2 Mo, je ne prévois aucun problème d'allocation de mémoire. Vous pouvez tout charger en mémoire et remplacer vos données.

Si de BC vous devez TOUJOURS remplacer pour AA, un String.Replace sera correct. Mais si vous avez besoin de plus de contrôle, vous pouvez utiliser un Regex.Replace:

var input  = "ABCDABCABCDABCABCDABCABCDABCD";
var output = Regex.Replace(input, "BC", (match) =>
{
    // here you can add some juice, like counters, etc
    return "AA";
});
0
Rubens Farias

Mon approche ressemble un peu à la modélisation: elle prend la chaîne d'entrée et extrait (supprime) les sous-chaînes à remplacer. Ensuite, il prend les parties restantes de la chaîne (le modèle) et les combine avec les nouvelles sous-chaînes de remplacement. Ceci est effectué dans une opération parallèle (modèle + chaque chaîne de remplacement), qui construit les chaînes de sortie.

Je pense que ce que j'explique ci-dessus peut être plus clair avec le code. Ceci utilise vos exemples d'entrées ci-dessus:

const char splitter = '\t';   // use a char that will not appear in your string

string input = "ABCDABCABCDABCABCDABCABCDABCD";
string oldString = "BC";
string[] newStrings = { "AA", "BB", "CC", "DD", "EE" };

// In input, replace oldString with tabs, so that we can do String.Split later
var inputTabbed = input.Replace(oldString, splitter.ToString());
// ABCDABCABCDABCABCDABCABCDABCD --> A\tDA\tA\tDA\tA\tDA\tA\tDA\tD

var inputs = inputTabbed.Split(splitter);
/* inputs (the template) now contains:
[0] "A" 
[1] "DA"
[2] "A" 
[3] "DA"
[4] "A" 
[5] "DA"
[6] "A" 
[7] "DA"
[8] "D" 
*/

// In parallel, build the output using the template (inputs)
// and the replacement strings (newStrings)
var outputs = new List<string>();
Parallel.ForEach(newStrings, iteration =>
    {
        var output = string.Join(iteration, inputs);
        // only lock the list operation
        lock (outputs) { outputs.Add(output); }
    });

foreach (var output in outputs)
    Console.WriteLine(output);

Sortie:

AAADAAAAAADAAAAAADAAAAAADAAAD
ABBDABBABBDABBABBDABBABBDABBD
ACCDACCACCDACCACCDACCACCDACCD
ADDDADDADDDADDADDDADDADDDADDD
AEEDAEEAEEDAEEAEEDAEEAEEDAEED

Pour que vous puissiez faire une comparaison, voici une méthode complète qui peut être utilisée dans le code de test par Erti-Chris Eelmaa:

private static void TemplatingImp(string input, string replaceWhat, IEnumerable<string> replaceIterations)
{
    const char splitter = '\t';   // use a char that will not appear in your string

    var inputTabbed = input.Replace(replaceWhat, splitter.ToString());
    var inputs = inputTabbed.Split(splitter);

    // In parallel, build the output using the split parts (inputs)
    // and the replacement strings (newStrings)
    //var outputs = new List<string>();
    Parallel.ForEach(replaceIterations, iteration =>
    {
        var output = string.Join(iteration, inputs);
    });
}
0
chue x

Vous n'obtiendrez probablement rien de plus rapide que String.Replace (sauf si vous devenez natif) car iirc String.Replace est implémenté dans le CLR lui-même pour des performances optimales. Si vous voulez des performances à 100%, vous pouvez interfacer facilement avec du code ASM natif via C++/CLI et ainsi de suite.

0
Nemo