【问题标题】:Why is str = str.Replace().Replace(); faster than str = str.Replace(); str = str.Replace()?为什么 str = str.Replace().Replace();比 str = str.Replace(); 快str = str.替换()?
【发布时间】:2018-06-22 01:32:01
【问题描述】:

我正在进行本地测试以比较 C# 中 String 和 StringBuilder 的替换操作性能,但对于 String,我使用以下代码:

String str = "String to be tested. String to be tested. String to be tested."
str = str.Replace("i", "in");
str = str.Replace("to", "ott");
str = str.Replace("St", "Tsr");
str = str.Replace(".", "\n");
str = str.Replace("be", "or be");
str = str.Replace("al", "xd");

但是,在注意到 String.Replace() 比 StringBuilder.Replace() 快后,我开始针对上面的代码测试以下代码:

String str = "String to be tested. String to be tested. String to be tested."
str = str.Replace("i", "in").Replace("to", "ott").Replace("St", "Tsr").Replace(".", "\n").Replace("be", "or be").Replace("al", "xd");

最后一个结果快了大约 10% 到 15% 倍,关于为什么它更快的任何想法?为同一个变量赋值会很昂贵吗?

【问题讨论】:

  • 您是否在发布模式下编译测试?
  • 我不确定,我知道我没有使用调试模式,我在 Visual Studio 中使用 ctrl + f5 运行它
  • > 最后一个结果快了大约 10% 到 15% 倍 你是如何测量的?
  • 我使用 StopWatch 来测量结果、启动它、进行替换和停止手表。

标签: c# string replace inline assign


【解决方案1】:

我已经制定了这个基准:

namespace StringReplace
{
    using BenchmarkDotNet.Attributes;
    using BenchmarkDotNet.Running;

    public class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run<Program>();
        }

        private String str = "String to be tested. String to be tested. String to be tested.";

        [Benchmark]
        public string Test1()
        {
            var a = str;
            a = a.Replace("i", "in");
            a = a.Replace("to", "ott");
            a = a.Replace("St", "Tsr");
            a = a.Replace(".", "\n");
            a = a.Replace("be", "or be");
            a = a.Replace("al", "xd");

            return a;
        }

        [Benchmark]
        public string Test2()
        {
            var a = str;
            a = a.Replace("i", "in").Replace("to", "ott").Replace("St", "Tsr").Replace(".", "\n").Replace("be", "or be").Replace("al", "xd");

            return a;
        }
    }
}

结果:

BenchmarkDotNet=v0.10.0
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-7700 CPU 3.60GHz, ProcessorCount=8
Frequency=3515629 Hz, Resolution=284.4441 ns, Timer=TSC
Host Runtime=Clr 4.0.30319.42000, Arch=32-bit RELEASE
GC=Concurrent Workstation
JitModules=clrjit-v4.7.2600.0
Job Runtime(s):
    Clr 4.0.30319.42000, Arch=32-bit RELEASE


 Method |      Mean |    StdDev |    Median |
------- |---------- |---------- |---------- |
  Test1 | 1.3768 us | 0.0354 us | 1.3704 us |
  Test2 | 1.3941 us | 0.0325 us | 1.3778 us |

如您所见,发布模式下的结果是相同的。因此,我认为由于变量分配过多,调试模式的差异可能很小。但是在发布模式下编译器可以优化它。

【讨论】:

    【解决方案2】:

    简答

    看起来您正在调试配置中进行编译。因为编译器需要保证源代码的每条语句都可以设置断点,所以多次赋值给本地的摘录效率较低。

    如果您在 Release 配置中编译,以不让您设置断点为代价优化代码生成,则两个摘录都编译为相同的中间代码,因此应该具有相同的性能。

    请注意,在调试或发布配置中进行编译与是否使用调试器 (F5) 从 Visual Studio 启动应用程序 (Ctrl + F5) 不一定相关。详情请见my answer here

    长答案

    C# 编译为 .NET 中间语言(IL、MSIL 或 CIL)。 .NET SDK 附带了一个工具,IL Disassembler,它可以向我们展示这种中间语言,以便更好地理解差异。请注意,.NET 运行时 (VES) 是堆栈机器 - 而不是寄存器,IL 在“操作数堆栈”上运行,在该“操作数堆栈”上推入和拉出值。对于这个问题,性质不太重要,但要知道评估堆栈是存储临时值的地方。

    反汇编第一个摘录,我在没有设置“优化代码”选项的情况下编译(即,我使用调试配置编译),显示如下代码:

      .locals init ([0] string str)
      IL_0000:  nop
      IL_0001:  ldstr      "String to be tested. String to be tested. String t" + "o be tested."
      IL_0006:  stloc.0
      IL_0007:  ldloc.0
      IL_0008:  ldstr      "i"
      IL_000d:  ldstr      "in"
      IL_0012:  callvirt   instance string [mscorlib]System.String::Replace(string, string)
      IL_0017:  stloc.0
      IL_0018:  ldloc.0
      IL_0019:  ldstr      "to"
      IL_001e:  ldstr      "ott"
      IL_0023:  callvirt   instance string [mscorlib]System.String::Replace(string, string)
    

    该方法有一个局部变量str。简而言之,摘录:

    1. 在评估堆栈 (ldstr) 上创建“要测试的字符串...”字符串。
    2. 将字符串存储到本地 (stloc.0),导致评估堆栈为空。
    3. 将该值从本地 (ldloc.0) 加载回堆栈。
    4. 使用另外两个字符串“i”和“in”(两个 ldstrcallvirt)对加载的值调用 Replace,从而生成一个仅包含结果字符串的评估堆栈。
    5. 将结果存储回本地 (stloc.0),导致评估堆栈为空。
    6. 从本地 (ldloc.0) 加载该值。
    7. 使用另外两个字符串“to”和“ott”(两个 ldstrcallvirt)对加载的值调用 Replace

    等等等等。

    比较第二个摘录,同样没有“优化代码”编译:

      .locals init ([0] string str)
      IL_0000:  nop
      IL_0001:  ldstr      "String to be tested. String to be tested. String t" + "o be tested."
      IL_0006:  stloc.0
      IL_0007:  ldloc.0
      IL_0008:  ldstr      "i"
      IL_000d:  ldstr      "in"
      IL_0012:  callvirt   instance string [mscorlib]System.String::Replace(string, string)
      IL_0017:  ldstr      "to"
      IL_001c:  ldstr      "ott"
      IL_0021:  callvirt   instance string [mscorlib]System.String::Replace(string, string)
    

    在第 4 步之后,评估堆栈具有第一次 Replace 调用的结果。由于本例中的 C# 代码不会将此中间值分配给 str 变量,因此 IL 可以避免存储和重新加载该值,而只是重新使用已在评估堆栈上的结果。 这会跳过第 5 步和第 6 步,从而使代码的性能稍微提高。

    但是等等,编译器肯定知道这些摘录是等价的,对吧?为什么它不总是产生第二组更有效的 IL 指令? 因为我编译时没有优化。因此,编译器假定我需要能够在每个 C# 语句上设置断点。在断点处,局部变量需要处于一致状态,并且评估堆栈需要为空。这就是为什么第一个摘录有第 5 步和第 6 步 - 以便调试器可以在这些步骤之间的断点处停止,我会看到 str local 具有我在该行上所期望的值。

    如果我编译这些摘录并优化(例如,我使用发布配置进行编译),那么编译器确实会为每个摘录生成相同的代码:

      // no .locals directive
      IL_0000:  ldstr      "String to be tested. String to be tested. String t" + "o be tested."
      IL_0005:  ldstr      "i"
      IL_000a:  ldstr      "in"
      IL_000f:  callvirt   instance string [mscorlib]System.String::Replace(string,strin g)
      IL_0014:  ldstr      "to"
      IL_0019:  ldstr      "ott"
      IL_001e:  callvirt   instance string [mscorlib]System.String::Replace(string, string)
    

    现在编译器知道我无法设置断点,它可以完全放弃使用本地,而让整个操作集只发生在评估堆栈上。因此,它可以跳过步骤 2、3、5 和 6,从而进一步优化代码。

    【讨论】:

    • 非常感谢您提供的详细信息!
    【解决方案3】:

    我不确定您的第二个代码幕后究竟发生了什么(或者它与第一个代码的背景有何不同)。但是,我猜您看到分配给同一个变量的速度较慢,因为string不可变的

    string 是不可变的意思:即使你为同一个变量分配了一个新值,你也在为此分配一个新的内存地址。也就是说,您可以想象为该新值保留了一个新变量,而垃圾收集器稍后会清理第一个值的内存位置。

    这是一个参考:

    有一个术语叫不可变,意思是对象的状态在创建后就不能改变。字符串是不可变的类型。字符串不可变的声明意味着,一旦创建,就不会通过更改分配给它的值来改变它。如果我们尝试通过连接(使用 + 运算符)来更改字符串的值或为其分配新值,实际上会导致创建一个新的字符串对象来保存对新生成的字符串的引用。看起来我们已经成功地改变了现有的字符串。但在幕后,创建了一个新的字符串引用,它指向新创建的字符串。

    https://www.c-sharpcorner.com/UploadFile/b1df45/string-is-immutable-in-C-Sharp/

    再次,这就是我的猜测,如果有人看到我错了,请发表评论。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-10-22
      • 2020-08-25
      • 2018-01-04
      • 2019-08-24
      • 2019-12-19
      • 1970-01-01
      • 1970-01-01
      • 2017-06-30
      相关资源
      最近更新 更多