简答
看起来您正在调试配置中进行编译。因为编译器需要保证源代码的每条语句都可以设置断点,所以多次赋值给本地的摘录效率较低。
如果您在 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。简而言之,摘录:
- 在评估堆栈 (
ldstr) 上创建“要测试的字符串...”字符串。
- 将字符串存储到本地 (
stloc.0),导致评估堆栈为空。
- 将该值从本地 (
ldloc.0) 加载回堆栈。
- 使用另外两个字符串“i”和“in”(两个
ldstr 和 callvirt)对加载的值调用 Replace,从而生成一个仅包含结果字符串的评估堆栈。
- 将结果存储回本地 (
stloc.0),导致评估堆栈为空。
- 从本地 (
ldloc.0) 加载该值。
- 使用另外两个字符串“to”和“ott”(两个
ldstr 和 callvirt)对加载的值调用 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,从而进一步优化代码。