我做了一个本地测试来比较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%,有什么想法可以解释为什么速度更快呢?将值赋值给相同的变量是否很昂贵?
发布于 2018-06-22 02:50:55
简短回答
它看起来像是在调试配置中进行编译。由于编译器需要确保源代码的每条语句都能在其上设置一个断点,因此多次分配给本地的摘录效率较低。
如果您在发行版配置中编译,这会优化代码生成,而不允许设置断点,则这两个摘录编译为相同的中间代码,因此应该具有相同的性能。
请注意,是否在调试或发布配置中编译并不一定与是否使用调试器(F5)从Visual启动应用程序(Ctrl + F5)有关。有关更多细节,请参见我在这里的回答。
长答案
C#可以编译成.NET中间语言(IL,或CIL或CIL)。.NET SDK附带了一个工具,IL反汇编程序,它可以向我们展示这种中间语言,以便更好地理解两者之间的区别。请注意,.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)将该值加载回堆栈。ldstr和callvirt) )调用已加载值的ldstr,从而生成一个只包含结果字符串的计算堆栈。stloc.0),从而生成一个空的计算堆栈。ldloc.0)加载该值。Replace和callvirt)调用加载值上的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本地有我在这一行中所期望的值。
如果我对这些摘录进行优化编译(例如,我使用发行版配置编译),那么编译器实际上会为每个代码生成相同的代码:
// 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,从而进一步优化代码。
发布于 2018-06-22 02:12:44
我制定了这个基准:
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 |正如您所看到的,结果在发布模式中是相同的。因此,我认为,由于过多的变量分配,调试模式的差异可能很小。但在发布模式下,编译器可以对其进行优化。
发布于 2018-06-22 02:07:35
我不知道在第二段代码的幕后到底发生了什么(或者它与第一段代码的背景有什么不同)。但是,我想您会看到分配给同一个变量要慢一些,因为string是不变的。
string是不可变的意思:即使将一个新值赋值给同一个变量,也要为此分配一个新的内存地址。也就是说,您可以想象为该新值保留了一个新变量,垃圾收集器稍后将清除第一个值的内存位置。
以下是这方面的参考:
有一个名为“不可变”的术语,这意味着在创建了一个对象之后,不能更改对象的状态。字符串是不可变的类型。字符串不可变的语句意味着,一旦创建了字符串,就不会通过更改分配给它的值来改变它。如果我们试图通过连接(使用+操作符)来更改字符串的值,或者给它分配一个新的值,它实际上会导致创建一个新的string对象来保存对新生成的字符串的引用。看来我们已经成功地改变了现有的字符串。但是在幕后,创建了一个新的字符串引用,它指向新创建的字符串。
https://www.c-sharpcorner.com/UploadFile/b1df45/string-is-immutable-in-C-Sharp/
再一次,我想,如果有人看到我错了,请留下评论。
https://stackoverflow.com/questions/50979466
复制相似问题