【问题标题】:Regex Match Performance Issue For Spintax ParserSpintax 解析器的正则表达式匹配性能问题
【发布时间】:2012-12-26 13:48:53
【问题描述】:

我正在编写一个应用程序,该应用程序旨在处理具有大量自旋税的数千篇文章/条目,格式如下:

{Hello|Hi} {World|There!}, how are you?

但是,当我使用分析器运行应用程序时,我注意到处理 Regex 的部分占用了大量资源,并且由于内存不足问题,我的应用程序最终崩溃。任何人都可以提出一种改进我的代码的方法或更好的解析 spintax 的方法吗?

public static String Spin(String text)
{
        Regex reg = new Regex(@"\{[^\{\}]*\}");
        Random rand = new Random((int)DateTime.Now.Ticks);
        while (true)
        {
            Match m = reg.Match(text);
            if (!m.Success) break;
            String[] parts = m.Value.TrimStart('{').TrimEnd('}').Split('|');
            int i = rand.Next(parts.Length);
            text = text.Substring(0, m.Index) + parts[i] + text.Substring(m.Index + m.Length);
        }
        return text;
  }

【问题讨论】:

  • 是您使用的实际正则表达式速度较慢还是代码的其余部分?你可以使用所有格量词来加速这个过程,以避免在给你带来麻烦时回溯。不过,我认为不会有太大变化。
  • 我认为特别是 regex.match 部分占用了最多的资源。其余代码没问题。
  • 我看到你的代码不支持嵌套的 spintaxes。是有意的吗?
  • 不,这不是有意的。它应该支持嵌套的 spintaxes 但我不知道如何实现这一点。
  • Spin 方法中的任何内容都不太可能导致内存泄漏。内存问题在您的代码中的其他地方。此外,如果这将被调用数千次,您应该将正则表达式设为静态(在方法外部定义),并在创建时使用 RegexOptions.Compiled 选项。

标签: c# regex spintax


【解决方案1】:

我已经实现了我的快速版本(没有Regex、没有Split、没有Substring、没有Replace 和其他字符串操作方法)。要复制字符串,我使用String.CopyTo 将符号复制到普通的char 数组中。

此代码完全支持嵌套 Spintaxes(可能无限深度)。一个限制是每个 Spintax 的最大选项数,目前是 100,但可以更改为 1000 或更多...另一个限制是输入字符串的最大长度,现在是 100000,但也可以增加。

关于性能 - 我的测试表明,此代码比任何优化的 Regex 解决方案(包括 Jim Mischel 的解决方案)快 15 倍以上,比使用 Substring 和其他字符串操作方法的版本快约 5 倍。我已经在 VS 2012 中使用优化代码设置在发布模式下对此进行了测试。

    static int[] partIndices = new int[100];
    static int[] depth = new int[100];
    static char[] symbolsOfTextProcessed = new char[100000];

    public static String SpinEvenMoreFaster(String text)
    {
        int cur = SpinEvenMoreFasterInner(text, 0, text.Length, 0);
        return new String(symbolsOfTextProcessed, 0, cur);
    }

    public static int SpinEvenMoreFasterInner(String text, int start, int end, int symbolIndex)
    {
        int last = start;
        for (int i = start; i < end; i++)
        {
            if (text[i] == '{')
            {
                int k = 1;
                int j = i + 1;
                int index = 0;
                partIndices[0] = i;
                depth[0] = 1;
                for (; j < end && k > 0; j++)
                {
                    if (text[j] == '{')
                        k++;
                    else if (text[j] == '}')
                        k--;
                    else if (text[j] == '|')
                    {
                        if (k == 1)
                        {
                            partIndices[++index] = j;
                            depth[index] = 1;
                        }
                        else
                            depth[index] = k;
                    }
                }
                if (k == 0)
                {
                    partIndices[++index] = j - 1;
                    int part = rand.Next(index);
                    text.CopyTo(last, symbolsOfTextProcessed, symbolIndex, i - last);
                    symbolIndex += i - last;
                    if (depth[part] == 1)
                    {
                        text.CopyTo(partIndices[part] + 1, symbolsOfTextProcessed, symbolIndex, partIndices[part + 1] - partIndices[part] - 1);
                        symbolIndex += partIndices[part + 1] - partIndices[part] - 1;
                    }
                    else
                    {
                        symbolIndex = SpinEvenMoreFasterInner(text, partIndices[part] + 1, partIndices[part + 1], symbolIndex);
                    }
                    i = j - 1;
                    last = j;
                }
            }
        }
        text.CopyTo(last, symbolsOfTextProcessed, symbolIndex, end - last);
        return symbolIndex + end - last;
    }

【讨论】:

  • 圣洁,!@#$!你是对的。这要快得多。 Ideone evidence here. SpinRE:03.760 秒。 SpinNoRE:00.795 秒。 SpinSergey:00.148 秒。 +1。
  • 谢谢:) 我很好奇 OP 是否还在监视这个页面:) 但问题本身很有趣!
  • 我上传了一张截图screencast.com/t/0Go7HHxTi,您可以在其中看到三个不同的微调器代码,而 SergeyS 是迄今为止最好的微调器代码。我希望你的代码@SergeyS,可以改进更多,因为我写了一个生成 1 到 N 级的代码,所有的微调器代码几乎不能到 20 级
【解决方案2】:

这是一个非正则表达式的替代方案。

2012-12-27 更新(见new ideone demo

  1. 优化了 OP 的代码,使用静态成员而不是在循环中声明变量,使用RegexOptions.Compiled,使用Substring 代替TrimLeftTrimRight。这些优化将 OP 代码的执行时间缩短了近 33%。
  2. 更新了 SpinNoRE 以处理任意嵌套的 spintax、优化代码并添加了 cmets。
  3. 为清楚起见,将SpinSpinFaster 分别重命名为SpinRESpinNoRE
  4. 使用嵌套示例更新了测试用例。 OP 的代码在处理嵌套的 spintaxes 时要慢得多(可以理解,因为每一层嵌套都会强制进行额外的正则表达式匹配)。

新的ideone demo 可用;下面的代码(cmets 在演示中可用;见链接):

public static String SpinNoRE(String text)
{
    int i, j, e = -1;
    char[] curls = new char[] {'{', '}'};
    text += '~';

    do
    {
        i =  e;
        e = -1;
        while ((i = text.IndexOf('{', i+1)) != -1)
        {
            j = i;
            while ((j = text.IndexOfAny(curls, j+1)) != -1 && text[j] != '}')
            {
                if (e == -1) e = i;
                i = j;
            }
            if (j != -1)
            {
                parts = text.Substring(i+1, (j-1)-(i+1-1)).Split('|');
                text = text.Remove(i, j-(i-1)).Insert(i, parts[rand.Next(parts.Length)]);
            }
        }
    }
    while (e-- != -1);

    return text.Remove(text.Length-1);
}

结果:

Input Text:       Oh! {{I'm|You're} here!|How are you{ doing{|, {buddy|pal|guy}}|}?}
Testing SpinRE:   Oh! You're here!
Testing SpinRE:   Oh! How are you doing?
Testing SpinRE:   Oh! How are you?
Testing SpinRE:   Oh! How are you doing, buddy?
Testing SpinRE:   Oh! I'm here!
Testing SpinRE:   Oh! How are you doing, guy?
Testing SpinRE:   Oh! How are you doing?
Testing SpinRE:   Oh! I'm here!
Testing SpinRE:   Oh! I'm here!
Testing SpinRE:   Oh! How are you doing?
Testing SpinNoRE: Oh! How are you doing, buddy?
Testing SpinNoRE: Oh! You're here!
Testing SpinNoRE: Oh! How are you?
Testing SpinNoRE: Oh! How are you?
Testing SpinNoRE: Oh! You're here!
Testing SpinNoRE: Oh! I'm here!
Testing SpinNoRE: Oh! How are you doing?
Testing SpinNoRE: Oh! How are you?
Testing SpinNoRE: Oh! How are you doing, buddy?
Testing SpinNoRE: Oh! I'm here!

Time elapsed over 100,000 runs of each in alternation:

SpinRE:           03.686s
SpinNoRE:         00.921s

(接触C#已经6年多了,如有错误请见谅。)

【讨论】:

  • 不适用于嵌套 Spintaxes。试试这个例子:“{{I'm|you're} here!|你好吗?}”
  • @SergeyS - 我只是想模拟 OP 的正则表达式解决方案,它也不支持嵌套的 Spintaxes。我更想证明没有正则表达式可以多快,而不是提供完整的 Spintax 解决方案。
  • @SergeyS - 我很抱歉。我最初阅读了您对 OP 关于他的代码不支持嵌套自旋税的问题的评论,我认为是这样。我正在更新我的答案。
  • 干得好!但是,我在您的解决方案中发现了一个小错误。试试这个字符串来重现它:“Curly {}{brackets|braces} here!”。如果有兴趣 - 看看我的代码,我没有使用 Split 和 Substring,它比你的快约 5 倍。
  • @SergeyS - 谢谢!我不会费心修复该错误,因为无论如何您的解决方案似乎都是优越的。我本人主要是 C/C++ 开发人员,所以我在 MSDN 上查找了 strposstrtok 等的等效函数,哈哈。不幸的是,我无法弄清楚如何获得“低”级别。
【解决方案3】:

我建议对您的代码进行一些更改。首先,将正则表达式定义移出方法并使用RegexOptions.Compiled 选项来减少每次调用的设置时间。此外,将随机数生成器的创建从大量使用的方法中移出。

此外,您可以通过告诉正则表达式从哪里开始匹配来消除许多不必要的字符串搜索。如果您最终对循环进行多次迭代,这一点很重要。这个想法是,如果您已经完成了字符串中位置 M 的替换,则没有理由检查这些匹配项,因为不会有任何匹配项。

您可以消除对TrimStartTrimEnd 的调用,方法是将表达式替换为:

String[] parts = m.Value.Substring(1, m.Value.Length-2).Split('|');

您已经知道该字符串以{ 开头并以} 结尾,并且中间没有这两个字符中的任何一个,所以您所要做的就是砍掉第一个和最后一个字符.没有理由承担由TrimStartTrimEnd 创建的临时字符串的成本。

另一种可能性是将捕获组添加到正则表达式(在要捕获的部分周围放置括号),并对捕获的文本而不是整个匹配的表达式进行操作。

将所有这些建议放在一起会导致:

static Regex reg = new Regex(@"\{([^\{\}]*)\}", RegexOptions.Compiled);
static Random rand = new Random();
public static String Spin(String text)
{
    int matchPos = 0;
    while (true)
    {
        Match m = reg.Match(text, matchPos);
        if (!m.Success) break;
        String[] parts = m.Groups[1].Value.Split('|');
        int i = rand.Next(parts.Length);
        text = text.Substring(0, m.Index) + parts[i] + text.Substring(m.Index + m.Length);
        matchPos = m.Index;
    }
    return text;
}

也就是说,这不支持嵌套,并且制作支持嵌套的正则表达式解决方案可能有些困难。它在速度方面也不是最佳的,因为它花费了大量时间来构建和重建text 字符串。稍加考虑,您可以对其进行更多优化,但它永远不会像SergyS supplied 这样的优化自定义解析器解决方案那么快。

如果速度是最重要的,那么您将需要一个自定义解析器。正则表达式版本不会那么快,但如果它足够快,它的好处是更小,并且比自定义解析器更容易理解和修改。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-07-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多