String.Replace()与StringBuilder.Replace()


77

我有一个字符串,我需要用字典中的值替换标记。它必须尽可能高效。用string.replace进行循环只会消耗内存(字符串是不可变的,请记住)。因为StringBuilder.Replace()旨在用于字符串操作,所以它会更好吗?

我希望避免花费RegEx,但是如果这样可以提高效率,那就可以了。

注意:我不在乎代码的复杂性,只在乎它的运行速度和消耗的内存。

平均状态:长度255-1024个字符,字典中有15-30个键。


标记和值的模式(长度)是什么?
Henk Holterman

短。标记5-15,价值5-25
达斯汀·戴维斯

Answers:


71

使用以下代码使用RedGate Profiler

class Program
    {
        static string data = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz";
        static Dictionary<string, string> values;

        static void Main(string[] args)
        {
            Console.WriteLine("Data length: " + data.Length);
            values = new Dictionary<string, string>()
            {
                { "ab", "aa" },
                { "jk", "jj" },
                { "lm", "ll" },
                { "yz", "zz" },
                { "ef", "ff" },
                { "st", "uu" },
                { "op", "pp" },
                { "x", "y" }
            };

            StringReplace(data);
            StringBuilderReplace1(data);
            StringBuilderReplace2(new StringBuilder(data, data.Length * 2));

            Console.ReadKey();
        }

        private static void StringReplace(string data)
        {
            foreach(string k in values.Keys)
            {
                data = data.Replace(k, values[k]);
            }
        }

        private static void StringBuilderReplace1(string data)
        {
            StringBuilder sb = new StringBuilder(data, data.Length * 2);
            foreach (string k in values.Keys)
            {
                sb.Replace(k, values[k]);
            }
        }

        private static void StringBuilderReplace2(StringBuilder data)
        {
            foreach (string k in values.Keys)
            {
                data.Replace(k, values[k]);
            }
        }
    }
  • String.Replace = 5.843ms
  • StringBuilder.Replace#1 = 4.059ms
  • Stringbuilder.Replace#2 = 0.461ms

字符串长度= 1456

stringbuilder#1在方法中创建stringbuilder,而#2则没有,因此性能差异最终可能是相同的,因为您只是将工作移出了方法。如果您以stringbuilder而不是字符串开头,那么可能会选择#2。

至于内存,使用RedGateMemory事件探查器,您无需担心,直到您进行很多替换操作为止,在这些操作中stringbuilder将会全面获胜。


如果编译器对此进行了优化,那么它将改变StringBuilderReplace1(data)的行吗?到StringBuilderReplace2(new StringBuilder(data,data.Length * 2));? 只是好奇。我了解其中的区别,如果您知道的话,这很好奇。
pqsk 2014年

1
我不明白为什么SB方法2这么快-JIT应该优化SB#1和SB#2,以便它们在运行时相同。

@Dai请记住,这是在2011年。此后情况可能有所变化。
达斯汀·戴维斯

7
@Dai-(延迟响应)-如答案中所述,探查器仅测量实际功能的经过时间。由于Replace#2中的stringbuilder声明在函数之外,因此构造时间不包括在经过的时间中。
Stirrblig'5

因此,您是说StringBuilderReplace1的89%的替换时间只是初始化StringBuilder实例?是否只有大约11%的时间(即4.059的0.461)用于密钥替换,如StringBuilderReplace2所示?如果是这样,我将分配一个StringBuilder缓冲区,并在一定程度上限制使用现有实例进行批处理的并行度。现在,问题是... StringBuilder.ToString添加了什么时间?因为公平地说,您的目标输出毕竟是一个字符串,并且只有第一个方法实际产生一个字符串。
Triynko

9

这可能会有帮助:

http://blogs.msdn.com/b/debuggingtoolbox/archive/2008/04/02/comparing-regex-replace-string-replace-and-stringbuilder-replace-which-has-better-performance.aspx

简短的答案似乎是String.Replace更快,尽管它可能会对您的内存占用/垃圾回收开销产生更大的影响。


有趣。根据他们的测试,string.replace更好。我在想,由于字符串string.replace的大小较小,考虑到创建字符串生成器的任何开销,它会更好
Dustin Davis

6

是的,StringBuilder可以同时提高速度和内存(基本上是因为每次您对其进行操作时,它都不会创建字符串的实例-StringBuilder始终对同一个对象进行操作)。这是带有一些详细信息的MSDN链接


但是值得创建字符串生成器的开销吗?
达斯汀·戴维斯

1
@Dustin:大概有15-30个替补。
汉克·霍尔特曼

@Henk-搜索15到30,不一定有很多替换。每个字符串的预期平均标记数很大。

@Joe:将进行15-30次扫描(也许Regex除外)。
Henk Holterman

@Henk,是的,但是String和StringBuilder的15-30次扫描的性能将相似。证明创建字符串生成器开销的任何性能差异都必须来自替换,而不是扫描。

5

stringbuilder.replace会比[String.Replace]更好吗?

是的,好多了。而且,如果您可以估计新字符串的上限(看起来可以),那么它可能足够快。

创建时,如下所示:

  var sb = new StringBuilder(inputString, pessimisticEstimate);

那么StringBuilder将不必重新分配其缓冲区。


1

将数据从String转换为StringBuilder并返回将需要一些时间。如果仅执行一次替换操作,则StringBuilder固有的效率提高可能无法弥补这一时间。另一方面,如果将字符串转换为StringBuilder,然后对其执行许多替换操作,最后将其转换回,则StringBuilder方法倾向于更快。


1

与其在整个字符串上执行15-30次替换操作,不如使用trie数据结构之类的内容来保存字典,可能会更有效率。然后,您可以遍历输入字符串一次以执行所有搜索/替换。


1

这将很大程度上取决于给定字符串中平均存在多少个标记。

在StringBuilder和String之间,搜索键的性能可能相似,但是如果您必须在单个字符串中替换许多标记,则StringBuilder将获胜。

如果平均每个字符串只期望一个或两个标记,而您的字典很小,那我就去买String.Replace。

如果标记很多,则可能需要定义一种自定义语法来标识标记-例如,用合适的转义规则将括号括在括号中。然后,您可以实现一种解析算法,该算法迭代字符串的字符一次,识别并替换找到的每个标记。或使用正则表达式。


正则表达式+1-请注意,如果执行此操作,则实际替换可以使用aMatchEvaluator来实际执行字典查找。
Random832

1

我在这里花了两分钱,我只写了几行代码来测试每种方法的执行方式,并且按预期,结果是“取决于”的。

对于较长的字符串Regex似乎表现更好,对于较短的字符串String.Replace则是如此。我可以看到的用法StringBuilder.Replace不是很有用,如果使用不当,则可能在GC方面具有致命性(我尝试共享一个实例StringBuilder)。

检查我的StringReplaceTests GitHub存储库


1

@DustinDavis的答案的问题在于,它对相同的字符串进行递归操作。除非您打算进行往返操作,否则在这种测试中,每个操作案例确实应该有单独的对象。

我决定创建自己的测试,因为我在Web上发现了一些矛盾的答案,并且我想完全确定自己。我正在处理的程序处理大量文本(在某些情况下具有成千上万行的文件)。

因此,这是一种快速的方法,您可以复制和粘贴并自己查看哪种方法更快。您可能需要创建自己的文本文件进行测试,但是您可以轻松地从任何地方复制和粘贴文本,并为自己制作足够大的文件:

using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Windows;

void StringReplace_vs_StringBuilderReplace( string file, string word1, string word2 )
{
    using( FileStream fileStream = new FileStream( file, FileMode.Open, FileAccess.Read ) )
    using( StreamReader streamReader = new StreamReader( fileStream, Encoding.UTF8 ) )
    {
        string text = streamReader.ReadToEnd(),
               @string = text;
        StringBuilder @StringBuilder = new StringBuilder( text );
        int iterations = 10000;

        Stopwatch watch1 = new Stopwatch.StartNew();
        for( int i = 0; i < iterations; i++ )
            if( i % 2 == 0 ) @string = @string.Replace( word1, word2 );
            else @string = @string.Replace( word2, word1 );
        watch1.Stop();
        double stringMilliseconds = watch1.ElapsedMilliseconds;

        Stopwatch watch2 = new Stopwatch.StartNew();
        for( int i = 0; i < iterations; i++ )
            if( i % 2 == 0 ) @StringBuilder = @StringBuilder .Replace( word1, word2 );
            else @StringBuilder = @StringBuilder .Replace( word2, word1 );
        watch2.Stop();
        double StringBuilderMilliseconds = watch1.ElapsedMilliseconds;

        MessageBox.Show( string.Format( "string.Replace: {0}\nStringBuilder.Replace: {1}",
                                        stringMilliseconds, StringBuilderMilliseconds ) );
    }
}

我得到了那个字符串。每次换出8-10个字母单词时,replace()的速度提高了约20%。如果您需要自己的经验证据,请自己尝试。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.