我正在做一个本地测试,以比较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%左右,为什么会有更快的想法呢?给同一个变量赋值很昂贵吗?
答案 0 :(得分:11)
我已经达到了基准:
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 |
如您所见,结果在发布模式下是相同的。因此,我认为由于过量分配变量,调试模式下的差异可能很小。但是在发布模式下,编译器可以对其进行优化。
答案 1 :(得分:5)
您似乎正在使用Debug配置进行编译。由于编译器需要确保每个源代码语句都可以在其上设置断点,因此多次分配给本地代码的摘录效率较低。
如果您在发布配置中进行编译,从而以不让您设置断点为代价优化了代码生成,则两个摘录都将编译为相同的中间代码,因此应该具有相同的性能。
请注意,是否在Debug或Release配置中进行编译与是否使用调试器(F5)从Visual Studio启动应用程序(Ctrl + F5)不一定相关。有关更多详细信息,请参见my answer here。
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
和ldstr
)在加载的值上调用callvirt
,导致仅包含结果字符串。stloc.0
)中,从而导致评估堆栈为空。ldloc.0
)加载该值。Replace
和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
局部值具有该行所期望的值。
如果我对这些摘录进行了优化(例如,我使用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,从而进一步优化代码。
答案 2 :(得分:0)
我不确定您的第二个代码在幕后到底发生了什么(或它在后台与第一个代码有什么不同)。但是,我想您会发现分配给同一变量的速度较慢,因为string
是不可变的。
string
是不可变的:即使将新值分配给相同的变量,也要为其分配新的内存地址。也就是说,您可以想象一个新变量正在为该新值保留,而第一个值的存储位置随后将由垃圾收集器清除。
以下是该参考:
有一个称为不可变的术语,表示创建对象后不能更改其状态。字符串是不可变的类型。字符串是不可变的声明表示,一旦创建字符串,就不会通过更改分配给它的值来对其进行更改。如果尝试通过串联(使用+运算符)更改字符串的值或为其分配新值,则实际上会导致创建一个新的字符串对象来保存对新生成的字符串的引用。似乎我们已经成功更改了现有字符串。但是在后台,将创建一个新的字符串引用,该引用指向新创建的字符串。
https://www.c-sharpcorner.com/UploadFile/b1df45/string-is-immutable-in-C-Sharp/
再次,这就是我的猜测,如果有人发现我错了,请发表评论。