在巨大的字符串中替换多个字符串的最快方法

时间:2013-11-26 15:14:38

标签: c# string performance replace

我正在寻找替换大(~1mb)字符串的多个(~500)子串的最快方法。无论我尝试过什么,似乎String.Replace是最快的方法。

我只关心最快的方式。不是代码可读性,可维护性等。我不在乎我是否需要使用不安全的代码或预处理原始字符串。

编辑:在评论之后,我添加了更多详细信息:

每次替换迭代都会将字符串上的ABC替换为其他字符串(每次替换迭代次数不同)。要替换的字符串总是相同的 - ABC将始终是ABC。从不ABD。因此,如果有400.000千位替换迭代。每次使用相同的字符串 - ABC - 将替换为其他(不同的)字符串。

我可以掌控ABC是什么。只要它不影响结果,我可以使它超短或超长。很明显ABC不能 hello 因为hello将在大多数输入字符串中作为单词存在。

示例输入:ABCDABCABCDABCABCDABCABCDABCD

示例替换为字符串:BC

用字符串替换示例:AA, BB, CC, DD, EE (5 iterations)

示例输出:

AAADAAAAAADAAAAAADAAAAAADAAAD
ABBDABBABBDABBABBDABBABBDABBD
ACCDACCACCDACCACCDACCACCDACCD
ADDDADDADDDADDADDDADDADDDADDD
AEEDAEEAEEDAEEAEEDAEEAEEDAEED

平均情况:输入字符串为100-200kb,替换迭代次数为40.000次。 最坏情况:输入字符串为1-2mb,替换迭代次数为400.000。

我可以做任何事情。并行做,不安全等等。我不知道怎么做。重要的是它需要尽可能快。

由于

8 个答案:

答案 0 :(得分:24)

由于我对这个问题有点兴趣,我制定了一些解决方案。通过硬核优化,可以进一步降低。

获取最新来源:https://github.com/ChrisEelmaa/StackOverflow/blob/master/FastReplacer.cs

输出

-------------------------------------------------------
| Implementation       | Average | Separate runs      |
|----------------------+---------+--------------------|
| Simple               |    3485 | 9002, 4497, 443, 0 |
| SimpleParallel       |    1298 | 3440, 1606, 146, 0 |
| ParallelSubstring    |     470 | 1259, 558, 64, 0   |
| Fredou unsafe        |     356 | 953, 431, 41, 0    |
| Unsafe+unmanaged_mem |      92 | 229, 114, 18, 8    |
-------------------------------------------------------

你可能不会在制作你自己的替换方法时击败.NET人员,它很可能已经使用不安全了。如果你用C完全写下来,我相信你可以把它降低两倍。

我的实施可能是错误的,但你可以得到一般的想法。

答案 1 :(得分:4)

使用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);
    }

答案 2 :(得分:1)

这听起来像是在对字符串进行标记? 我会考虑生成一个缓冲区并索引你的标记。 或使用模板引擎

作为一个天真的例子,你可以使用代码生成来制作以下方法

public string Produce(string tokenValue){

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

    return builder.ToString();

}

如果您运行迭代次数足够多,那么构建模板的时间将为自己付出代价。然后,您也可以并行调用该方法,没有副作用。 还要看看你的字符串实习

答案 3 :(得分:1)

我对Fredou的代码进行了修改,因为它适用于int*而不是char*,因此需要较少的比较。对于n长度的字符串,它仍然需要n次迭代,它只需做较少的比较。如果字符串整齐地对齐2(因此要替换的字符串只能出现在索引0,2,4,6,8等)或甚至n/2(如果它已对齐),则可以进行n/4次迭代4(你使用long*)。我不是很擅长这样的小摆弄,所以有人可能会在我的代码中找到一些可能更有效的明显缺陷。我确认我的变体结果与简单string.Replace的结果相同。

此外,我预计可以在500x string.Copy中获得一些收益,但尚未对此进行调查。

我的结果(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

所以大约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]并且可以使用此代码也可以使用三个字母的字符串,如果是int字段则不可能。

它还需要对Program.cs进行一些更改,所以这里有完整的要点:

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

编辑:我不确定为什么我的ParallelSubstring这么慢。我在发布模式下运行.NET 4,没有调试器,无论是x86还是x64。

答案 4 :(得分:0)

由于您的输入字符串可能长达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";
});

答案 5 :(得分:0)

你可能不会得到比String.Replace更快的东西(除非你是原生的)因为iirc String.Replace是在CLR本身实现的,以获得最大的性能。如果您希望获得100%的性能,可以通过C ++ / CLI方便地与本机ASM代码连接,并从那里开始。

答案 6 :(得分:0)

我的方法有点像模板 - 它接受输入字符串并拉出(移除)要替换的子字符串。然后它将获取字符串的剩余部分(模板)并将它们与新的替换子字符串组合在一起。这是在并行操作(模板+每个替换字符串)中完成的,它构建输出字符串。

我认为我上面解释的内容可能会更加清晰。这使用了上面的示例输入:

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

答案 7 :(得分:0)

我在一个项目上遇到了类似的问题,我实施了一个Regex解决方案,对文件执行多个不区分大小写的替换。

出于提高效率的目的,我将标准设置为仅使用原始字符串

我发布了一个简单的控制台应用来测试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()(CASE SENSITIVE):2ms
  • 使用一次性多次替换的Regex单次传递(不区分大小写):8ms
  • 使用ReplaceIgnoreCase扩展(不区分大小写):55ms
  • 循环替换