我正在做一个本地测试来比较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
。简而言之,摘录:
ldstr
)上创建“要测试的字符串...”字符串。stloc.0
)中,从而导致评估堆栈为空。ldloc.0
)加载回堆栈。Replace
用另外两个字符串“ i”和“ in”(两个ldstr
和callvirt
)调用加载的值,从而导致仅包含结果字符串的评估堆栈。stloc.0
),从而导致评估堆栈为空。ldloc.0
)加载该值。Replace
用另外两个字符串“ to”和“ ott”(两个ldstr
和callvirt
)调用加载的值。等等等等。
与第二节摘录相比,第二节摘录也没有“优化代码”进行编译:
.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具有该行期望的值。
如果我对这些摘录进行了优化(例如,我使用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] 删除。
我来说两句