web-dev-qa-db-ja.com

巨大な文字列内の複数の文字列を置き換える最速の方法

大きな(〜1mb)文字列の複数(〜500)の部分文字列を置き換える最速の方法を探しています。私が試したことは何でも、String.Replaceがそれを行う最も速い方法であるようです。

最速の方法を気にしています。コードの可読性、保守性などではありません。安全でないコードを使用したり、元の文字列を前処理したりする必要があるかどうかは気にしません。

編集:コメントの後に、さらに詳細を追加しました。

各置換反復では、文字列のABCを他の文字列で置換します(置換反復ごとに異なります)。置き換える文字列は常に同じです-ABCは常にABCです。決してABD。したがって、_400.000_がある場合、数千回の反復が置換されます。同じ文字列-ABC-は毎回他の(異なる)文字列に置き換えられます。

私はABCが何であるかを制御することができます。結果に影響を与えない限り、超短または超長にすることができます。明らかにABCをhelloにすることはできません。これは、helloがほとんどの入力文字列にWordとして存在するためです。

入力例:ABCDABCABCDABCABCDABCABCDABCD

文字列からの置換例:BC

文字列で置換する例:AA, BB, CC, DD, EE (5 iterations)

出力例:

_AAADAAAAAADAAAAAADAAAAAADAAAD
ABBDABBABBDABBABBDABBABBDABBD
ACCDACCACCDACCACCDACCACCDACCD
ADDDADDADDDADDADDDADDADDDADDD
AEEDAEEAEEDAEEAEEDAEEAEEDAEED
_

平均の場合:入力文字列は100-200kbで、40.000回の置換が繰り返されます。最悪の場合:入力文字列は1〜2 mbで、400.000回の置換が繰り返されます。

私は何でもできます。並行して実行したり、安全でなかったりします。どのように実行してもかまいません。重要なのは、できるだけ速くなる必要があるということです。

ありがとう

31
Yannis

私はこの問題に少し興味を持っていたので、いくつかの解決策を作成しました。筋金入りの最適化により、さらに下がることが可能です。

最新のソースを取得するには: https://github.com/ChrisEelmaa/StackOverflow/blob/master/FastReplacer.cs

そして出力

 --------------------------------------------- ---------- 
 |実装|平均|別の実行| 
 | ---------------------- + --------- + -------- ------------ | 
 |シンプル| 3485 | 9002、4497、443、0 | 
 | SimpleParallel | 1298 | 3440、1606、146、0 | 
 | ParallelSubstring | 470 | 1259、558、64、0 | 
 |安全でないFredou | 356 | 953、431、41、0 | 
 | Unsafe + unmanaged_mem | 92 | 229、114、18、8 | 
 ------------------------------------- ------------------ 

独自のreplaceメソッドを作成する際に、おそらく.NETの連中を倒すことはないでしょう。おそらく、すでに安全でないものを使用しています。完全にCで記述すれば、2倍に削減できると思います。

私の実装はバグがあるかもしれませんが、一般的なアイデアを得ることができます。

25

unsafeを使用してx64としてコンパイル

結果:

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

Erti-Chris Eelmaaのコードを取得し、以前のコードをこれに置き換えます。

私は別のイテレーションをするつもりはないと思いますが、私は危険なことでいくつかのことを学びました、それは良いことです:-)

    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

文字列をトークン化しているようですね?バッファを作成し、トークンにインデックスを付けることを検討します。またはテンプレートエンジンを使用する

単純な例として、コード生成を使用して次のメソッドを作成できます

public string Produce(string tokenValue){

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

    return builder.ToString();

}

イテレーションを十分に実行している場合、テンプレートを構築するための時間はそれ自体で元が取れます。その後、副作用なしでそのメソッドを並行して呼び出すこともできます。文字列のインターンも見てください

1
Adam Mills

int*ではなくchar*で機能するため、比較が少なくて済むようにFredouのコードを変更しました。それでも、nの長さの文字列に対してnの反復が必要ですが、比較する必要が少ないだけです。文字列が2で整然と配置されている場合(つまり、置換する文字列はインデックス0、2、4、6、8などでのみ発生する場合)、またはn/2である場合は、n/4の反復が可能です。 4倍(long*を使用します)。私はこのように少しいじるのがあまり得意ではないので、誰かが私のコードにもっと効率的ないくつかの明らかな欠陥を見つけることができるかもしれません。バリエーションの結果が単純なstring.Replaceの結果と同じであることを確認しました。

さらに、500x string.Copyでいくつかの利益が得られることを期待していますが、まだ検討していません。

私の結果(フレドゥ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

したがって、約2/3の時間(x86、ただしx64はほぼ同じ)。

このコードの場合:

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;
      }
    }
  }
}

固定バッファを含む構造体はここでは厳密に必要ではなく、単純なintフィールドで置き換えることができますが、char[2]char[3]に拡張すると、このコードを3文字の文字列もintフィールドの場合は不可能です。

Program.csにもいくつかの変更が必要なため、ここに完全な要点を示します。

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

編集:私のParallelSubstringがなぜとても遅いのかわかりません。 .NET 4をリリースモードで実行しています。デバッガはなく、x86またはx64のどちらでも実行しています。

1
JulianR

私はプロジェクトで同様の問題があり、ファイルで複数のおよび大文字小文字を区別しない置換を実行するように正規表現ソリューションを実装しました。

効率を上げるために、元の文字列を1回だけ通過するように条件を設定しました

いくつかの戦略をテストするための簡単なコンソールアプリを公開しました https://github.com/nmcc/Spikes/tree/master/StringMultipleReplacements

Regexソリューションのコードは次のようになります。

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]));

編集:私のコンピューターでテストアプリケーションを実行する実行時間は次のとおりです:

  • String.Substring()(大文字と小文字を区別)を呼び出す置換をループする:2ms
  • 一度に複数の置換を行う正規表現を使用したシングルパス(大文字と小文字は区別されません):8ミリ秒
  • ReplaceIgnoreCase Extension(大文字と小文字を区別しない)を使用して置換をループする:55ms
0
nunoc

入力文字列は2Mbまでの長さになる可能性があるため、メモリ割り当ての問題は発生しません。すべてをメモリにロードして、データを置き換えることができます。

BCから常にAAを置き換える必要がある場合は、String.Replace 大丈夫だろう。ただし、より詳細な制御が必要な場合は、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

私のアプローチはテンプレートに少し似ています。入力文字列を受け取り、置換する部分文字列を引き出します(削除します)。次に、文字列(テンプレート)の残りの部分を取得し、それらを新しい置換部分文字列と結合します。これは、出力文字列を作成する並列操作(テンプレート+各置換文字列)で行われます。

私が上記で説明していることはコードでより明確になると思います。これは、上記のサンプル入力を使用します。

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);

出力:

AAADAAAAAADAAAAAADAAAAAADAAAD
ABBDABBABBDABBABBDABBABBDABBD
ACCDACCACCDACCACCDACCACCDACCD
ADDDADDADDDADDADDDADDADDDADDD
AEEDAEEAEEDAEEAEEDAEEAEEDAEED

比較ができるように、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

Iirc String.ReplaceはCLR自体に最大のパフォーマンスを実現するために実装されているため、おそらくString.Replaceよりも速くなることはありません(ネイティブに移行しない限り)。 100%のパフォーマンスが必要な場合は、C++/CLIを介してネイティブASMコードと簡単にインターフェースし、そこから移動できます。

0
Nemo