如何优化这个正则表达式替换?

7

经过大量的测量,我已经确定了一个需要优化的窗口服务热点。我们正在处理可能包含多个连续空格的字符串,并且我们希望将其减少为仅有单个空格。我们使用一个静态编译的正则表达式来完成这个任务:

private static readonly Regex 
    regex_select_all_multiple_whitespace_chars = 
        new Regex(@"\s+",RegexOptions.Compiled);

然后按以下方式使用它:
var cleanString=
    regex_select_all_multiple_whitespace_chars.Replace(dirtyString.Trim(), " ");

这行代码被调用了数百万次,而且非常密集。我尝试编写更好的代码,但是一时想不出更好的解决方案。鉴于正则表达式的处理需求相当适中,肯定有更快的方法。是否可以使用指针进行unsafe处理来进一步提速?

编辑:

感谢对这个问题的惊人回复...非常出乎意料!


你是经常在较小的字符串上运行它,还是在巨大的字符串上运行它? - Rob Fonseca-Ensor
@rob,它正在处理大约10-40个字符长的字符串。 - spender
8个回答

8

这个快了大约三倍:

private static string RemoveDuplicateSpaces(string text) {
  StringBuilder b = new StringBuilder(text.Length);
  bool space = false;
  foreach (char c in text) {
    if (c == ' ') {
      if (!space) b.Append(c);
      space = true;
    } else {
      b.Append(c);
      space = false;
    }
  }
  return b.ToString();
}

我选择了这个变体。可以通过使用for(int i...)而不是foreach,并使用char c=text[i]来获得更快的速度。再次感谢大家。 - spender

7

这个怎么样...

public string RemoveMultiSpace(string test)
{
var words = test.Split(new char[] { ' ' }, 
    StringSplitOptions.RemoveEmptyEntries);
return string.Join(" ", words);
}

NUnit运行测试用例:
测试时间以毫秒为单位。

Regex Test time: 338,8885
RemoveMultiSpace Test time: 78,9335

private static readonly Regex regex_select_all_multiple_whitespace_chars =
   new Regex(@"\s+", RegexOptions.Compiled);

[Test]
public void Test()
{
    string startString = "A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      ";
    string cleanString;
    Trace.WriteLine("Regex Test start");
    int count = 10000;
    Stopwatch timer = new Stopwatch();
    timer.Start();
    for (int i = 0; i < count; i++)
    {
        cleanString = regex_select_all_multiple_whitespace_chars.Replace(startString, " ");
    }
    var elapsed = timer.Elapsed;
    Trace.WriteLine("Regex Test end");
    Trace.WriteLine("Regex Test time: " + elapsed.TotalMilliseconds);

    Trace.WriteLine("RemoveMultiSpace Test start");
    timer = new Stopwatch();
    timer.Start();
    for (int i = 0; i < count; i++)
    {
        cleanString = RemoveMultiSpace(startString);
    }
    elapsed = timer.Elapsed;
    Trace.WriteLine("RemoveMultiSpace Test end");
    Trace.WriteLine("RemoveMultiSpace Test time: " + elapsed.TotalMilliseconds);
}

public string RemoveMultiSpace(string test)
{
    var words = test.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
    return string.Join(" ", words);
}

编辑:
进行了更多测试并添加了Guffa的“RemoveDuplicateSpaces”方法,基于StringBuilder。
因此我的结论是,当有许多空格时,StringBuilder方法更快,但在较少的空格下,字符串分割方法略微更快。

Cleaning file with about 30000 lines, 10 iterations
RegEx time elapsed: 608,0623
RemoveMultiSpace time elapsed: 239,2049
RemoveDuplicateSpaces time elapsed: 307,2044

Cleaning string, 10000 iterations:
A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      A B  C   D    E     F      
RegEx time elapsed: 590,3626
RemoveMultiSpace time elapsed: 159,4547
RemoveDuplicateSpaces time elapsed: 137,6816

Cleaning string, 10000 iterations:
A      B      C      D      E      F      A      B      C      D      E      F      A      B      C      D      E      F      A      B      C      D      E      F      A      B      C      D      E      F      A      B      C      D      E      F      A      B      C      D      E      F      A      B      C      D      E      F      
RegEx time elapsed: 290,5666
RemoveMultiSpace time elapsed: 64,6776
RemoveDuplicateSpaces time elapsed: 52,4732


非常好。一个快速的浅层测试表明这比StringBuilder方法更快。 - Kobi
更新:抱歉,一开始我忘记了RemoveMultiSpace方法的循环。 - Jens Granlund
我觉得测试结果看起来太好了。;) 它比使用 StringBuilder 快一点,但也会创建一堆临时字符串。你需要尝试一些真实数据来看看它如何影响性能。 - Guffa
有人知道为什么这种方法比使用 StringBuilder 更快吗? - Heinzi
@Guffa,我用一个大约有30000行的C#文件(RReference.cs)进行了测试,虽然没有太多空格,但速度仍然快了3倍。 - Jens Granlund
1
@Heinzi:它在框架中使用了优化的(不安全模式或非托管)方法,比使用StringBuilder的托管代码更快。尽管Split-Join做了一些额外的工作,在某些情况下仍然更快。 - Guffa

6

目前,您正在用另一个单个空格替换单个空格。尝试匹配\s{2,}(或类似的内容,如果您想替换单个换行符和其他字符)。


我同意。对于关键点,快速测试表明我们可以做得更好。 - Kobi
可能是因为代码更改与性能比率非常好。只需在正则表达式中更改几个字符,就可以提高25%的性能。 :) - Guffa

3

您不能使用正则表达式。例如:

private static string NormalizeWhitespace(string test)
{
    string trimmed = test.Trim();

    var sb = new StringBuilder(trimmed.Length);

    int i = 0;
    while (i < trimmed.Length)
    {
        if (trimmed[i] == ' ')
        {
            sb.Append(trimmed[i]);

            do { i++; } while (i < trimmed.Length && trimmed[i] == ' ');
        }

        sb.Append(trimmed[i]);

        i++;
    }

    return sb.ToString();
}

使用此方法和以下测试平台:

private static readonly Regex MultipleWhitespaceRegex = new Regex(
    @"\s+", 
    RegexOptions.Compiled);

static void Main(string[] args)
{
    string test = "regex  select    all multiple     whitespace   chars";

    const int Iterations = 15000;

    var sw = new Stopwatch();

    sw.Start();
    for (int i = 0; i < Iterations; i++)
    {
        NormalizeWhitespace(test);
    }
    sw.Stop();
    Console.WriteLine("{0}ms", sw.ElapsedMilliseconds);

    sw.Reset();

    sw.Start();
    for (int i = 0; i < Iterations; i++)
    {
        MultipleWhitespaceRegex.Replace(test, " ");
    }
    sw.Stop();
    Console.WriteLine("{0}ms", sw.ElapsedMilliseconds);
}

我得到了以下结果:
// NormalizeWhitespace - 27ms
// Regex - 132ms

请注意,这仅是针对一个非常简单的示例进行测试的,可以通过删除对String.Trim的调用进一步优化,并且仅提供此内容以说明正则表达式有时不是最佳答案。

除了空白字符不仅仅是 ' ',以及你没有使用变量 trimemd 这两个事实之外,这正是我要建议的。+1 - Rob Fonseca-Ensor
@Rob Fonseca-Ensor,感谢您发现这个问题,那是最后一刻未完成的更改。 :) - João Angelo

3
我很好奇一个直接的实现会表现如何:
    static string RemoveConsecutiveSpaces(string input)
    {
        bool whiteSpaceWritten = false;
        StringBuilder sbOutput = new StringBuilder(input.Length);

        foreach (Char c in input)
        {
            if (c == ' ')
            {
                if (!whiteSpaceWritten)
                {
                    whiteSpaceWritten = true;
                    sbOutput.Append(c);
                }
            }
            else
            {
                whiteSpaceWritten = false;
                sbOutput.Append(c);
            }
        }

        return sbOutput.ToString();
    }

它的速度大约是正则表达式的三倍。看看我的答案(它基本上有相同的代码)。 - Guffa

3
只是一个建议,如果您的数据没有Unicode空格,请使用[ \r\n]+[ \n]+或仅使用 +(如果只有空格),基本上将其限制为最小字符集。

@" {2,}"在我的数据上表现几乎与此处建议的最佳方法一样好(在10%以内),因此是一个非常好的答案! - spender

0

由于这是一个非常简单的表达式,只需用单个空格替换两个或多个空格,摆脱Regex对象并自己硬编码替换(在C++/CLI中):

String ^text = "Some   text  to process";
bool spaces = false;
// make the following static and just clear it rather than reallocating it every time
System::Text::StringBuilder ^output = gcnew System::Text::StringBuilder;
for (int i = 0, l = text->Length ; i < l ; ++i)
{
  if (spaces)
  {
    if (text [i] != ' ')
    {
      output->Append (text [i]);
      spaces = false;
    }
  }
  else
  {
    output->Append (text [i]);
    if (text [i] == ' ')
    {
      spaces = true;
    }
  }
}
text = output->ToString ();

嗯,不确定是否有必要使用C++/CLI! - Mitch Wheat
语法略有不同,但将上述内容更改为C#并不太难。我只是打开了一个C++/CLI项目来测试代码(是的,我知道,但这是我必须使用的)。 - Skizz

0

数组总是更快

        public static string RemoveMultiSpace(string input)
    {
        var value = input;

        if (!string.IsNullOrEmpty(input))
        {
            var isSpace = false;
            var index = 0;
            var length = input.Length;
            var tempArray = new char[length];
            for (int i = 0; i < length; i++)
            {
                var symbol = input[i];
                if (symbol == ' ')
                {
                    if (!isSpace)
                    {
                        tempArray[index++] = symbol;
                    }
                    isSpace = true;
                }
                else
                {
                    tempArray[index++] = symbol;
                    isSpace = false;
                }
            }
            value = new string(tempArray, 0, index);
        }

        return value;
    }

var 关键字的过度使用。为什么要将数组复制到另一个数组中,而不是直接从 tempArray 创建字符串? - Guffa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接