繁体   English   中英

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

[英]Fastest way to replace multiple strings in a huge string

我正在寻找替换大(~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 次替换迭代。

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

由于我对这个问题有点兴趣,所以我制定了一些解决方案。 通过核心优化,可能会下降更多。

获取最新源码: 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 人员,它很可能已经在使用 unsafe。 我相信如果你完全用 C 语言编写它,你可以把它降低两倍。

我的实现可能有问题,但您可以大致了解。

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

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

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

public string Produce(string tokenValue){

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

    return builder.ToString();

}

如果您运行迭代次数足够多,则构建模板的时间将是物有所值的。 然后,您还可以并行调用该方法,而不会产生副作用。 还要看看你的字符串实习

您可能不会获得比 String.Replace 更快的任何东西(除非您使用本机),因为 iirc String.Replace 是在 CLR 本身中实现的,以获得最佳性能。 如果您想要 100% 的性能,您可以通过 C++/CLI 方便地与本机 ASM 代码交互并从那里开始。

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

此外,我希望在 500x string.Copy一些收益。 string.Copy它确实如此,但还没有研究过。

我的结果(弗雷多二世):

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 这么慢。 我在 x86 或 x64 中以 Release 模式运行 .NET 4,没有调试器。

我有一个项目类似的问题,我已经实现了一个正则表达式的解决方案,以在文件上执行多个不区分大小写的替代品。

出于效率目的,我设置了通过原始字符串一次的标准

我已经发布了一个简单的控制台应用程序来测试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
  • 一次使用正则表达式进行多次替换(不区分大小写):8ms
  • 使用ReplaceIgnoreCase扩展(不区分大小写)循环替换:55 毫秒

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

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

我认为我上面解释的代码可能会更清楚。 这使用您从上面的示例输入:

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM