【问题标题】:String Interpolation vs String.Format字符串插值与 String.Format
【发布时间】:2015-11-27 07:50:07
【问题描述】:

使用字符串插值是否有明显的性能差异:

myString += $"{x:x2}";

vs String.Format()?

myString += String.Format("{0:x2}", x);

我只是问,因为 Resharper 正在提示修复,而我之前被愚弄了。

【问题讨论】:

  • @Blorgbeard 老实说,我很懒惰。而且我认为,如果你们中的一个正直的男人/女人立即知道答案,那将花费更少的时间。
  • 我喜欢当我第一次问这个问题时,它被否决了,而现在,两年后,它达到了 +21。
  • 说真的。怎么会有人怀疑这个问题的用处呢?如果提出这个问题的每个人都必须“亲自尝试看看”,你能想象完全浪费的工时吗?即使只用了 5 分钟,也可以乘以迄今为止查看过此问题的 10,000 多名开发人员。当同事怀疑你的结果时,你会怎么做?重新做一遍吗?或者也许只是将他们推荐给这个 SO 帖子。这就是它的用途。
  • @BTownTKD 这对你来说是典型的 Stackoverflow 行为。如果有人出于预期目的使用该网站,他们会立即被疏远。这也是我认为应该允许我们集体封号的原因之一。许多人根本不配出现在这个网站上。
  • 我认为轻浮是不会改变含义或上下文的东西(例如,错字)。但是在标题中添加关键词会使问题更容易找到。这可能对未来的读者有所帮助,这对我来说似乎一点也不重要。就目前而言,正如我所提到的,标题相当广泛和模糊。

标签: c# string performance resharper string-interpolation


【解决方案1】:

您应该注意到,在 C#10 和 .NET 6 中对字符串插值进行了重大优化 - String Interpolation in C# 10 and .NET 6

我一直在迁移我对字符串格式的所有使用,以及对字符串连接的使用,以使用字符串插值。

我同样关心不同方法之间的内存分配差异,如果不是更多的话。我发现在处理少量字符串时,字符串插值几乎总是在速度和内存分配方面都获胜。如果您的字符串数量不确定(在设计时未知),则应始终使用 System.Text.StringBuilder.Append(xxx)System.Text.StringBuilder.AppendFormat(xxx)

另外,我会指出您使用+= 进行字符串连接。要非常小心,并且只对 少量 小字符串这样做。

【讨论】:

    【解决方案2】:

    也许迟到了,但没有发现其他人提到它:我注意到您的问题中有 += 运算符。看起来您正在创建循环执行此操作的某些十六进制输出。

    在字符串 (+=) 上使用 concat,尤其是在循环中可能会导致难以找到的问题:分析转储时的 OutOfMemoryException 将显示内部有大量可用内存!

    会发生什么?

    1. 内存管理将为结果字符串寻找足够的连续空间。
    2. 那里写的串联字符串。
    3. 释放了用于存储原始左侧变量值的空间。

    请注意,第 1 步中分配的空间肯定比第 3 步中释放的空间大。

    在下一个循环中会发生同样的事情,依此类推。 假设在每个周期中将 10 字节长的字符串添加到原来 20 字节长的字符串 3 次,我们的内存会是什么样子?

    [20 字节空闲]X1[30 字节空闲]X2[40 字节空闲]X2[50 字节分配]

    (因为几乎可以肯定在我放置的循环中还有其他命令在使用内存 Xn-s 来演示它们的内存分配。这些可能会被释放或仍然分配,跟我来。)

    如果在下一次分配 MM 发现没有足够大的连续内存(60 字节) 然后它会尝试从操作系统或通过重组其插座中的空闲空间来获取它。 X1 和 X2 将被移动到某个地方(如果可能),并且 20+30+40 连续块变得可用。耗时但可用。

    但是如果块大小达到 88kb(谷歌为什么是 88kb),它们将被分配到 大对象堆。此处的空闲块将不再被压缩。

    因此,如果您的字符串 += 操作结果超过此大小(例如,您正在构建 CSV 文件或以这种方式在内存中呈现某些内容),上述循环将导致大小不断增长的空闲内存块,总和它们可以是千兆字节,但您的应用程序将因 OOM 而终止,因为它无法分配可能小至 1Mb 的块,因为没有一个块足够大:)

    抱歉,解释太长了,但它发生在几年前,这是一个艰难的教训。从那时起,我一直在反对不恰当地使用字符串连接。

    【讨论】:

      【解决方案3】:

      答案是肯定的和否定的。 ReSharper 通过不显示 第三个​​ 变体来欺骗您,这也是性能最高的。列出的两个变体产生相同的 IL 代码,但以下确实会有所提升:

      myString += $"{x.ToString("x2")}";
      

      完整的测试代码

      using BenchmarkDotNet.Attributes;
      using BenchmarkDotNet.Configs;
      using BenchmarkDotNet.Diagnosers;
      using BenchmarkDotNet.Diagnostics.Windows;
      using BenchmarkDotNet.Running;
      
      namespace StringFormatPerformanceTest
      {
          [Config(typeof(Config))]
          public class StringTests
          {
              private class Config : ManualConfig
              {
                  public Config() => AddDiagnoser(MemoryDiagnoser.Default, new EtwProfiler());
              }
      
              [Params(42, 1337)]
              public int Data;
      
              [Benchmark] public string Format() => string.Format("{0:x2}", Data);
              [Benchmark] public string Interpolate() => $"{Data:x2}";
              [Benchmark] public string InterpolateExplicit() => $"{Data.ToString("x2")}";
          }
      
          class Program
          {
              static void Main(string[] args)
              {
                  var summary = BenchmarkRunner.Run<StringTests>();
              }
          }
      }
      

      测试结果

      |              Method | Data |      Mean |  Gen 0 | Allocated |
      |-------------------- |----- |----------:|-------:|----------:|
      |              Format |   42 | 118.03 ns | 0.0178 |      56 B |
      |         Interpolate |   42 | 118.36 ns | 0.0178 |      56 B |
      | InterpolateExplicit |   42 |  37.01 ns | 0.0102 |      32 B |
      |              Format | 1337 | 117.46 ns | 0.0176 |      56 B |
      |         Interpolate | 1337 | 113.86 ns | 0.0178 |      56 B |
      | InterpolateExplicit | 1337 |  38.73 ns | 0.0102 |      32 B |
      

      InterpolateExplicit() 方法更快,因为我们现在明确告诉编译器使用string。无需box 将要格式化的对象。拳击确实很费钱。另外请注意,我们稍微减少了分配。

      【讨论】:

      • 我正在更新您作为答案,因为您提供了基准等。干得好!
      • 如果xnull第三个 变体将会崩溃。
      • 为什么在这种情况下使用插值,而不仅仅是myString += x.ToString("x2");
      • 查看反编译后的IL code sharplab.io/...。 Format 和 Interpolate 具有完全相同的内容。它使用 string.Format 函数。 InterpolateExplicit 中的字符串插值是多余的,它简单地调用 Data.ToString("x2")... 并检查是否为空。奇怪... Data.ToString("x2") 可以返回 null 吗?
      • 我发现了有趣的博文@meziantou Interpolated strings: advanced usages
      【解决方案4】:

      问题是关于性能的,但是标题只是说“vs”,所以我觉得必须再补充几点,但其中一些是固执己见的。

      • 本地化

        • 字符串插值由于其内联代码性质而无法本地化。在本地化之前,它已变成string.Format。但是,有相应的工具(例如ReSharper)。
      • 可维护性(我的看法)

        • string.Format 更具可读性,因为它专注于我想要表达的句子,例如在构建漂亮且有意义的错误消息时。使用 {N} 占位符给了我更大的灵活性,而且以后更容易修改。
        • 此外,插值中的内联格式说明符很容易被误读,并且在更改期间很容易与表达式一起删除。
        • 使用复杂且长的表达式时,插值很快就会变得更加难以阅读和维护,因此从这个意义上说,当代码不断发展并变得更加复杂时,它无法很好地扩展。 string.Format 不太容易出现这种情况。
        • 归根结底,这一切都与关注点分离有关:我不喜欢将应该如何呈现应该呈现什么混为一谈。

      因此,基于这些,我决定在我的大部分代码中坚持使用string.Format。不过,我已经准备了一个扩展方法来获得一种更流畅的编码方式,我更喜欢这种方式。扩展的实现是单行的,在使用中看起来很简单。

      var myErrorMessage = "Value must be less than {0:0.00} for field {1}".FormatWith(maximum, fieldName);
      

      插值是一个很棒的功能,不要误会我的意思。但是 IMO 它在那些错过 string.Format 类似功能的语言中表现最好,例如 JavaScript。

      【讨论】:

      • 感谢您添加此内容。
      • 我不同意可维护性;授予 ReSharper 可以更轻松地将插入的值与其相应的索引进行匹配(反之亦然),但我认为确定 {3} 是 X 还是 Y 仍然需要更多的认知负担,尤其是在您开始重新排列格式时。 Madlibs 示例:$"It was a {adjective} day in {month} when I {didSomething}" vs string.Format("It was a {0} day in {1} when I {2}", adjective, month, didSomething) --> $"I {didSomething} on a {adjective} {month} day" vs string.Format("I {2} on a {0} {1} day", adjective, month, didSomething)
      • @drzaus 感谢您分享您的想法。你有好点子,但是只有当我们只使用简单的、命名良好的局部变量时它才是正确的。我见过很多次是复杂的表达式、函数调用,以及任何放入插值字符串的东西。对于string.Format,我认为您不太容易出现这个问题。但无论如何,这就是为什么我强调这是我的意见:)
      【解决方案5】:

      字符串插值在编译时转换为 string.Format()。

      同样在 string.Format 中,您可以为单个参数指定多个输出,并为单个参数指定不同的输出格式。但我猜字符串插值更具可读性。所以,这取决于你。

      a = string.Format("Due date is {0:M/d/yy} at {0:h:mm}", someComplexObject.someObject.someProperty);
      
      b = $"Due date is {someComplexObject.someObject.someProperty:M/d/yy} at {someComplexObject.someObject.someProperty:h:mm}";
      

      有一些性能测试结果https://koukia.ca/string-interpolation-vs-string-format-string-concat-and-string-builder-performance-benchmarks-c1dad38032a

      【讨论】:

      • 字符串插值只是有时变成String::Format。有时进入String::Concat。而且该页面上的性能测试并不真正有意义:您传递给每个方法的参数数量是依赖的。 concat 并不总是最快的,stringbuilder 并不总是最慢的。
      【解决方案6】:

      值得注意的是相对的。但是:字符串插值在编译时会转换为string.Format(),因此它们最终应该得到相同的结果。

      但存在细微差别:从this 问题中可以看出,格式说明符中的字符串连接会导致额外的string.Concat() 调用。

      【讨论】:

      • 实际上,字符串插值在某些情况下可以编译为字符串连接(例如,当使用int 时)。 var a = "hello"; var b = $"{a} world"; 编译为字符串连接。 var a = "hello"; var b = $"{a} world {1}"; 编译为字符串格式。
      猜你喜欢
      • 2015-10-03
      • 2010-10-29
      • 2016-01-22
      • 1970-01-01
      • 2022-07-05
      • 2018-02-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多