【问题标题】:When escaping characters in a string, is it faster to test for the need to do so first?当转义字符串中的字符时,首先测试是否需要这样做更快?
【发布时间】:2024-01-23 06:10:01
【问题描述】:

如果没有要转义的字符,会

if (s.Contains("\"")) 
    s = s.Replace("\"", "\"\""); 

真的比跑得更快

s = s.Replace("\"", "\"\"");

Contains 方法必须像 Replace 方法一样搜索字符串,如果 Replace 方法没有找到任何要转义的内容,那么我认为它不应该比 Contains 方法花费的时间更长查看字符串。但是如果有一个字符要转义,那么你就必须在字符串中搜索两次。

【问题讨论】:

  • 您可以随时测试它并告诉我们。 :)
  • 如果这是您唯一的性能问题,那么您就是一个快乐的人 ;=)
  • 为什么要这样做?您是否正在创建某种代码生成器?变量中的字符串实际上不需要转义。只有字符串文字需要转义。我错过了什么吗?
  • @ChrisDunaway 首先想到的两个例子是我们应该将字符串视为正则表达式模式的一部分,并将应视为文字的字符串写入XML 属性。
  • @ChrisDunaway 正在将数据导出到 Csv。我刚刚看到有人用第一种方式写它,感觉想从工作中休息 5 分钟来对其进行微观分析:)

标签: c# performance string-comparison


【解决方案1】:

一些基本的性能测试,每做 10 000 000 次(大约 30 个字符的字符串,中间有一个引号),给出:

Contains("\"")          1327 ms.
Contains('"')           2949 ms.
Replace("\"", "\"\"")   2528 m.s

Contains 调用大约需要Replace 调用的一半时间。我原以为查找字符会比查找字符串快,但没想到却并非如此。

因此,比较首先检查并始终替换以达到收支平衡的情况,其中 x 是调用 Contains 的时间,y 是您需要进行替换的频率:

x + 2xy = 2x

给予:

y = 0.5

这意味着这两种方法的收支平衡在 50% 左右。如果您需要在超过一半的时间内进行替换,则首先检查存在性没有任何好处。

【讨论】:

  • 我做了类似的测试,得到了类似的结果。我的结论是,在你一次做大约 10,000,000+ 次之前,不值得额外的代码行进行微优化。不过这只是我个人的情况,因为我通常更喜欢让我的代码保持简短和简洁,只要性能提升微不足道,就会有降低性能的风险。
  • 只是为了增加一点洞察力,Replace 在您的情况下花费两倍的时间是因为引号在字符串的中间。这意味着 contains 将获得一半,看到报价,然后返回。更换还是要检查另一半。这两个函数实际上使用相同的搜索算法,因此在搜索方面,两者都不是更快。
  • 还有一些关于最后一点的跟进:@BrandonMoore 还应该考虑到他的琴弦的性质。如果存在某种模式来说明引用的位置(如果存在),则答案可能会改变。例如,如果字符串总是将引号作为第一个或第二个字母(如果有的话),Contains 将(可能)明显更快
  • @PhillipSchmidt:如果我将引号放在字符串的末尾,我会得到非常相似的结果。即使他们使用相同的搜索算法,Contains 方法也不会创建新的字符串,这就解释了性能差异。将引号放在字符串的开头只会使 Contains 调用快约 25%。
  • 另外请记住,Replace 只有在首先找到匹配项时才会创建一个新字符串。所以这也必须考虑在内。该死的,@BrandonMoore,你会让我做数学题。
【解决方案2】:

Andre Calil 的微基准已修改:

        List<string> StringList = new List<string>();

        for (int i = 0; i < 10000; i++)
        {
            StringList.Add(DateTime.Now.Ticks + " abc ");
        }

        string temp;

        KeyValuePair<string, string>[] StringsToReplace = new KeyValuePair<string, string>[6];

        // Use array to avoid dictionary access cost
        StringsToReplace[0] = new KeyValuePair<string,string>("1", ".1.");
        StringsToReplace[1] = new KeyValuePair<string,string>("a", "z");
        StringsToReplace[2] = new KeyValuePair<string,string>("b", "x");
        StringsToReplace[3] = new KeyValuePair<string,string>("c", "v");
        StringsToReplace[4] = new KeyValuePair<string,string>("d", "u");
        StringsToReplace[5] = new KeyValuePair<string,string>("e", "t");

        int TotalIterations = 100;
        Stopwatch stopWatch1 = new Stopwatch();
        Stopwatch stopWatch2 = new Stopwatch();

        GC.Collect(); // remove influence of garbage objects

        for (int j = 0; j <= TotalIterations; j++)
        {
            stopWatch1.Start(); // StopWatch Start/Stop does its own accumation

            for (int i = 0; i < StringList.Count; i++)
            {
                for (int k = 0; k < StringsToReplace.Length; k++)
                {
                    temp = StringList[i].Replace(StringsToReplace[k].Value, StringsToReplace[k].Key);
                }
            }

            stopWatch1.Stop();

            stopWatch2.Start();

            for (int i = 0; i < StringList.Count; i++)
            {
                for (int k = 0; k < StringsToReplace.Length; k++)
                {
                    if (StringList[i].Contains(StringsToReplace[k].Value))
                        temp = StringList[i].Replace(StringsToReplace[k].Value, StringsToReplace[k].Key);
                }
            }

            stopWatch2.Stop();

            if (j == 0) // discard first run, warm only
            {
                stopWatch1.Reset();
                stopWatch2.Reset();
            }
        }

        // Elapsed.TotalMilliseconds return in double, more accurate than ElapsedMilliseconds
        Console.WriteLine("Replace           : {0:N3} ms", stopWatch1.Elapsed.TotalMilliseconds / TotalIterations);
        Console.WriteLine("Contains > Replace: {0:N3} ms", stopWatch2.Elapsed.TotalMilliseconds / TotalIterations);

结果支持直接调用 Replace,因为 50% 的时间需要真正的替换:

Replace           : 7.453 ms
Contains > Replace: 8.381 ms

【讨论】:

  • 我不同意 KeyValuePair[] vc Dictionary,但使用 2 个秒表和双倍是聪明的。为此 +1
【解决方案3】:

这取决于需要修复的字符串与所有字符串的比例。如果必须修复很少的字符串,这肯定是一个胜利。尽管如果没有找到任何匹配项,Replace 肯定不会分配新字符串,但胜利将非常小。这是一种常见的优化。

不过,无法预测收支平衡点。测量。

【讨论】:

    【解决方案4】:

    与所有与性能相关的问题一样,答案是衡量

    这取决于Replace 的智能程度(它是否“提前”分配任何内存?如果是,那对性能的影响有多大?),取决于需要替换的字符串与字符串总数的比率您根据字符串的长度处理第一个" 所在的索引(Contains 如果可以更快地返回true 则更快)等。

    【讨论】:

      【解决方案5】:

      String.Replace

      此方法执行一个序数(区分大小写和 文化不敏感)搜索以查找 oldValue。

      String.Contains

      此方法执行一个序数(区分大小写和 文化不敏感)比较。搜索从第一个开始 此字符串的字符位置并继续到最后 字符位置。

      所以,我会说没有实际的区别。不过,我将在这里运行一个微基准测试。


      微观基准测试(伙计,我喜欢这个)

      代码:

              List<string> StringList = new List<string>();
      
              for (int i = 0; i < 10000; i++)
              {
                  StringList.Add(DateTime.Now.Ticks + " abc ");
              }
      
              string temp;
      
              Dictionary<string, string> StringsToReplace = new Dictionary<string, string>();
              StringsToReplace.Add("1", ".1.");
              StringsToReplace.Add("a", "z");
              StringsToReplace.Add("b", "x");
              StringsToReplace.Add("c", "v");
              StringsToReplace.Add("d", "u");
              StringsToReplace.Add("e", "t");
      
              long ReplaceElapsedTime = 0;
              long ContainsReplaceElapsedTime = 0;
      
              int TotalIterations = 10000;
      
              for (int j = 0; j < TotalIterations; j++)
              {
                  Stopwatch stopWatch = new Stopwatch();
                  stopWatch.Start();
      
                  for (int i = 0; i < StringList.Count; i++)
                  {
                      foreach (KeyValuePair<string, string> CurrentPair in StringsToReplace)
                      {
                          temp = StringList[i].Replace(CurrentPair.Value, CurrentPair.Key);
                      }
                  }
      
                  stopWatch.Stop();
      
                  ReplaceElapsedTime += stopWatch.ElapsedMilliseconds;
      
                  stopWatch.Reset();
                  stopWatch.Start();
      
                  for (int i = 0; i < StringList.Count; i++)
                  {
                      foreach (KeyValuePair<string, string> CurrentPair in StringsToReplace)
                      {
                          if (StringList[i].Contains(CurrentPair.Value))
                              temp = StringList[i].Replace(CurrentPair.Value, CurrentPair.Key);
                      }
                  }
      
                  stopWatch.Stop();
      
                  ContainsReplaceElapsedTime += stopWatch.ElapsedMilliseconds;
              }
      
              Console.WriteLine(string.Format("Replace: {0} ms", ReplaceElapsedTime/TotalIterations));
              Console.WriteLine(string.Format("Contains > Replace: {0} ms", ContainsReplaceElapsedTime/TotalIterations));
      
              Console.ReadLine();
      

      结果:

      Replace: 14 ms 
      Contains > Replace: 14 ms
      

      =)

      【讨论】:

      • 您的微基准测试需要一些改进:1) 将测量移出循环 2) 避免使用字典枚举,3) 不要使用返回 int 的 ElapsedMillionseconds。
      • @FengYuan 感谢您的反馈,但是:1)我想要衡量每个交互,这就是我将其保留在循环中的原因 2)Dictionary 不是@ 987654328@,这是获取要替换的字符串的最快方法,并且 3) ElapsedMilliseconds 返回 long,这是正确的时间测量形式
      【解决方案6】:

      如果有人好奇(你不是,相信我),找出需要替换 OP 方法以提高效率的字符串的必要百分比的方程式(大致)是这样的:

      地点:

      C = Time it takes to create a new string
      R = average Ratio of Delimiter position to string length
      n = average string length
      p = average position of the delimiter (quote in this case) in the string
      t = time it takes to search one character of a string
      

      因此,如果您的字符串中包含引号的百分比大于此值 - 无论如何,请使用第一种方法。

      这并没有考虑我认为可以忽略不计的变量,例如返回时间、字符的实际替换(尽管您确实可以将其包含在 C 中)或任何我不考虑的编译器优化知道关于。

      附带说明:我只花了半个小时解决世界上最没用的方程式,这就是我从数学转向 CS 的一个完美例子。

      【讨论】:

        【解决方案7】:

        新版本:

                string test = "A quick brown fox jumps over a lazy dog.";
        
                int count = 1000 * 1000;
        
                Stopwatch watch = new Stopwatch();
        
                for (int i = 0; i < 4; i++)
                {
                    string result = String.Empty;
        
                    watch.Restart();
        
                    for (int c = 0; c < count; c++)
                    {
                        switch (i)
                        {
                            case 0: // warmup
                                break;
        
                            case 1:
                                if (test.Contains("\""))
                                {
                                    result = test.Replace("\"", "\"\"");
                                }
                                break;
        
                            case 2:
                                result = test.Replace("\"", "\"\"");
                                break;
        
                            case 3:
                                if (test.IndexOf('\"') >= 0)
                                {
                                    result = test.Replace("\"", "\"\"");
                                }
                                break;
                        }
                    }
        
                    watch.Stop();
        
                    Console.WriteLine("Test {0,16} {1,7:N3} ms {2}", 
                        new string[]{"Base","Contains-Replace","Replace","IndexOf-Replace"}[i],
                        watch.Elapsed.TotalMilliseconds,
                        result);
        

        结果:

        Test             Base   3.026 ms
        Test Contains-Replace 284.780 ms
        Test          Replace 214.503 ms
        Test  IndexOf-Replace  64.447 ms
        

        所以 Contains(string) 本身很慢。原因是由于 NLS(自然语言处理 API。但 IndexOf(char) 快得多。

        【讨论】:

          最近更新 更多