在.NET中链接的字符串替换

时间:2016-06-25 19:22:33

标签: c# regex string replace stringbuilder

给定:base64字符串

何时:Foo被称为

然后:Foo返回带有以下字符替换('A' => '_', 'B' => '-', 'C' => '+')的输入字符串,并尽可能快地执行

我比较了几种算法来确定哪个版本的Foo更快。结果指向普通的string.Replace,这是非常令人惊讶的。我原本期望正则表达式能够在编译时获得最初的打击,但随后会在string.Replace的每次调用中创建三个字符串副本,并且优于Foo

我想检查是否有其他人可以证实这些调查结果,或者想出为什么胜利者胜过其他人的原因。

我使用这些算法运行了Foo 100k次,结果是TimeSpan,在调试版本中使用StopWatch测量完成后执行:

00:00:00.0500790 <=== string.Replace [1]
00:00:00.0699696 <=== StringBuilder.Append [2]
00:00:00.0988960 <=== StringBuilder.Replace [3]
00:00:00.7440135 <=== Regex [4]

[1]:

Foo(string input) 
{ 
  return input.Replace("A", "_").Replace("B", "-").Replace("C", "+"); 
}

[2]:

Foo(string input)
{
    var sb = new StringBuilder(input.Length);
    foreach (var x in input)
    {
        if (x == 'A')
        {
            sb.Append('_');
        }
        else if (x == 'B')
        {
            sb.Append('-');
        }
        else if (x == 'C')
        {
            sb.Append('+');
        }
        else
        {
            sb.Append(x);
        }
    }
    return sb.ToString();
}

[3]:

Foo(string input) 
{
  return new StringBuilder(input, input.Length).Replace("A", "_").Replace("B", "-").Replace("C", "+").ToString()
}

[4]:

static readonly Regex charsRegex = new Regex(@"[ABC]", RegexOptions.Compiled);
Foo(string input)
{
  charsRegex.Replace(input, delegate (Match m)
    {
        var value = m.Value;
        if (value == "A")
        {
            return "_";
        }
        else if (value == "B")
        {
            return "-";
        }
        else if (value == "C")
        {
            return "+";
        }

        return value;
    });
}

4 个答案:

答案 0 :(得分:2)

我想建议其他实现。

public /*unsafe*/ static string Foo(string text)
{
    char[] a = text.ToCharArray();
    for(int i = 0; i < a.Length; i++)
        switch(a[i])
        {
        case 'A': a[i] = '_'; break;
        case 'B': a[i] = '-'; break;
        case 'C': a[i] = '+'; break;
        }
    return new string(a);
}

OR

public /*unsafe*/ static string Foo(string text)
{
    char[] a = new char[text.Length];
    for(int i = 0; i < text.Length; i++)
    {
        char c=text[i];
        switch(c)
        {
        case 'A': a[i] = '_'; break;
        case 'B': a[i] = '-'; break;
        case 'C': a[i] = '+'; break;
        default: a[i] = c; break;
        }
    }
    return new string(a);
}

如果您允许不安全的代码并取消注释不安全,则可能会比[1]更快。

[1]获胜,因为它全部是原生的,尽管通过数据有3个循环 [2]许多指数检查和当前指数增加 [3]多个循环通过相同的数据,许多索引检查,但可以进行原位替换) [4]最后,因为状态机的开销,并调用替换方法。加上字符串比较,但不是字符比较。

答案 1 :(得分:1)

在我看来,正则表达式简直复杂得多。

String.Replace直接调用Win32,并且可能会执行基于指针的字符串操作,以防止碎片等(很难确定 - 但它不会在托管代码中执行此操作)。如果我启动ILSpy,我会看到RegEx.Replace会执行大量的边界检查,然后执行匹配,然后使用StringBuilder执行对您的委托的调用结果。

答案 2 :(得分:1)

如果我们检查您指定的方法的实现,我们最终不会感到惊讶。

Regex.Replace包括模式匹配和大量字符串连接,这会导致开销。普通的旧String.Replace直接使用C ++实现(comstring.cpp文件),这是低级的,很可能是非常优化的。

答案 3 :(得分:1)

是的,我对你发现的东西感到惊讶......并不是说正则表达式是最慢的,这是预期的......但链式String.Replace表现得和StringBuilder一样。 我做了一些我自己的检查我比较你做的那样[1],但是我修改了[2]以使它接近裸骨实现,因为我可以O(n)。

[1]

    static string Foo(string input)
    {
        string result = input.Replace("A", "_");
        result = result.Replace("B", "-");
        result = result.Replace("C", "+");
        return result;
    }

[2]

    static string Foo2(string input)
    {
        var length = input.Length;
        var sb = new char[length];
        for (int i = 0; i < length; i++)
        {
            switch (input[i])
            {
                case 'A':
                    sb[i] = '_';
                    break;
                case 'B':
                    sb[i] = '-';
                    break;
                case 'C':
                    sb[i] = '+';
                    break;
                default:
                    sb[i] = input[i];
                    break;
            }
        }
        return sb.ToString();
    }

我的测试字符串长度超过700万个字符(7230872,Lorem Ipsum)。 所以我们可以注意到几点:

  1. 这两种方法的执行速度非常快(55ms vs 51ms aprox)因为其他进程和CPU可用性的这种活动在最终结果中起着重要作用。我执行了这两种方法100次并总结了所有的执行时间,并为Foo获得了5500ms,而Foo2则为5100.
  2. Foo方法至少要经过整个字符串3次(可能更多,不确定)。另一方面, Foo2方法看起来只是通过字符串一次...但事实并非如此......它至少经过两次...... input.Length也经历过它(计算字符数)。谢谢PhillipH
  3. 当您尝试替换越来越多的字符FooFoo2之间的差异变得更加明显。使用15个字符替换Foo在aprox 240ms和Foo2中执行约60ms。
  4. 所以......总之......这里没有魔法,只是执行得非常快......:)