首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >为什么str = str.Replace().Replace();比str =str.Replace()更快;str = str.Replace()?

为什么str = str.Replace().Replace();比str =str.Replace()更快;str = str.Replace()?
EN

Stack Overflow用户
提问于 2018-06-22 01:32:01
回答 3查看 281关注 0票数 4

我做了一个本地测试来比较C#中String和StringBuilder的替换操作性能,但是对于String,我使用了以下代码:

代码语言:javascript
复制
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()更快之后,我继续根据上面的代码测试以下代码:

代码语言:javascript
复制
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%,有什么想法可以解释为什么速度更快呢?将值赋值给相同的变量是否很昂贵?

EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2018-06-22 02:50:55

简短回答

它看起来像是在调试配置中进行编译。由于编译器需要确保源代码的每条语句都能在其上设置一个断点,因此多次分配给本地的摘录效率较低。

如果您在发行版配置中编译,这会优化代码生成,而不允许设置断点,则这两个摘录编译为相同的中间代码,因此应该具有相同的性能。

请注意,是否在调试或发布配置中编译并不一定与是否使用调试器(F5)从Visual启动应用程序(Ctrl + F5)有关。有关更多细节,请参见我在这里的回答

长答案

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

解压缩第一个摘录,我编译它时没有设置“优化代码”选项(即,我使用Debug配置编译),显示了如下代码:

代码语言:javascript
复制
  .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. 使用另外两个字符串( "i“和" in”(两个ldstrcallvirt) )调用已加载值的ldstr,从而生成一个只包含结果字符串的计算堆栈。
  5. 将结果存储回本地(stloc.0),从而生成一个空的计算堆栈。
  6. 从本地(ldloc.0)加载该值。
  7. 用另外两个字符串"to“和"ott”(两个Replacecallvirt)调用加载值上的callvirt

以此类推。

与第二段相比,在没有“优化代码”的情况下也进行了编译:

代码语言:javascript
复制
  .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本地有我在这一行中所期望的值。

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

代码语言:javascript
复制
  // 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,从而进一步优化代码。

票数 5
EN

Stack Overflow用户

发布于 2018-06-22 02:12:44

我制定了这个基准:

代码语言:javascript
复制
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;
        }
    }
}

结果:

代码语言:javascript
复制
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 |

正如您所看到的,结果在发布模式中是相同的。因此,我认为,由于过多的变量分配,调试模式的差异可能很小。但在发布模式下,编译器可以对其进行优化。

票数 11
EN

Stack Overflow用户

发布于 2018-06-22 02:07:35

我不知道在第二段代码的幕后到底发生了什么(或者它与第一段代码的背景有什么不同)。但是,我想您会看到分配给同一个变量要慢一些,因为string不变的

string是不可变的意思:即使将一个新值赋值给同一个变量,也要为此分配一个新的内存地址。也就是说,您可以想象为该新值保留了一个新变量,垃圾收集器稍后将清除第一个值的内存位置。

以下是这方面的参考:

有一个名为“不可变”的术语,这意味着在创建了一个对象之后,不能更改对象的状态。字符串是不可变的类型。字符串不可变的语句意味着,一旦创建了字符串,就不会通过更改分配给它的值来改变它。如果我们试图通过连接(使用+操作符)来更改字符串的值,或者给它分配一个新的值,它实际上会导致创建一个新的string对象来保存对新生成的字符串的引用。看来我们已经成功地改变了现有的字符串。但是在幕后,创建了一个新的字符串引用,它指向新创建的字符串。

https://www.c-sharpcorner.com/UploadFile/b1df45/string-is-immutable-in-C-Sharp/

再一次,我想,如果有人看到我错了,请留下评论。

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/50979466

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档