为什么是str = str.Replace()。Replace(); 快于str = str.Replace(); str = str.Replace()?

迪戈贝。

我正在做一个本地测试来比较C#中String和StringBuilder的Replace操作性能,但是对于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%左右,为什么会有更快的想法呢?给同一个变量赋值很昂贵吗?

乔·塞维尔

简短答案

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

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

请注意,是否在调试或发布配置中进行编译与是否通过调试器(F5)从Visual Studio启动应用程序(Ctrl + F5)无关。有关更多详细信息,请在此处查看我的答案

长答案

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

反汇编我未设置“优化代码”选项(即,我使用Debug配置进行编译)而编译的第一节摘录,显示了如下代码:

  .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. Replace用另外两个字符串“ i”和“ in”(两个ldstrcallvirt调用加载的值,从而导致仅包含结果字符串的评估堆栈。
  5. 将结果存储回本地(stloc.0),从而导致评估堆栈为空。
  6. 从本地(ldloc.0加载该值
  7. Replace用另外两个字符串“ to”和“ ott”(两个ldstrcallvirt调用加载的值

等等等等。

与第二节摘录相比,第二节摘录也没有“优化代码”进行编译:

  .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步的原因-以便调试器可以在这些步骤之间的断点处停止,并且我将看到strlocal具有该行期望的值。

如果我对这些摘录进行了优化(例如,我使用Release配置进行编译),则实际上编译器会为每个代码生成相同的代码:

  // 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,从而进一步优化代码。

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章