字符串插值与字符串格式

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

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

格式() ?

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

我这么问只是因为 Resharper 正在提示修复,我以前也被愚弄过。

55057 次浏览

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

但是有一些细微的差别: 正如我们从 这个问题中可以看出的那样,格式说明符中的字符串连接会导致额外的 string.Concat()调用。

字符串插值转换成字符串。

还有绳子。格式可以为单个参数指定多个输出,为单个参数指定不同的输出格式。但我想字符串插值更易读。所以,这取决于你。

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

这个问题是关于性能的,但是标题只是说“ 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);

插值是一个伟大的功能,不要误会我。但是在那些缺少类似 string.Format特性的语言中,例如 JavaScript,它表现得最好。

答案既是肯定的,也是否定的。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 |

新的测试结果(.NET6)

重新运行 .NET 6.0.9.41905, X64 RyuJIT AVX2的测试。

|              Method | Data |     Mean |   Gen0 | Allocated |
|-------------------- |----- |---------:|-------:|----------:|
|              Format |   42 | 37.47 ns | 0.0089 |      56 B |
|         Interpolate |   42 | 57.61 ns | 0.0050 |      32 B |
| InterpolateExplicit |   42 | 11.46 ns | 0.0051 |      32 B |
|              Format | 1337 | 39.49 ns | 0.0089 |      56 B |
|         Interpolate | 1337 | 59.98 ns | 0.0050 |      32 B |
| InterpolateExplicit | 1337 | 12.85 ns | 0.0051 |      32 B |

InterpolateExplicit()方法更快,因为我们现在明确地告诉编译器使用 string。不需要 盒子对象被格式化。拳击的确很贵。另外,请注意,在 NET6之前,我们稍微减少了一点分配。

也许太晚提到,但没有发现其他人提到它: 我注意到 +=操作员在你的问题。看起来你正在创建一些在循环中执行这个操作的东西的十六进制输出。

在字符串上使用 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 (google 为什么是88kb) ,它们将被分配到 大型对象堆。这里的空闲块将不再被压缩。

所以如果你的字符串 + = 操作结果超过了这个大小(例如,你正在构建一个 CSV 文件或者以这种方式在内存中渲染某些东西) ,上面的循环将导致大量的空闲内存不断增长,这些空闲内存的总和可能是千兆字节,但是你的应用将以 OOM 结束,因为它不能分配一个可能只有1Mb 大小的块,因为没有一个块大到足以容纳它:)

对不起,解释了很长时间,但它发生在几年前,这是一个艰难的教训。从那以后,我一直反对不恰当地使用弦连音。

您应该注意到,在 C # 10和.NET 6-C # 10和.NET 6中的字符串插值中对 String 插值进行了显著的优化。

我不仅迁移了字符串格式化的所有用法,还迁移了字符串串联的所有用法,以便使用 String 插值。

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

另外,我还要说明 +=在字符串连接方面的用法。一定要非常小心,并且只对 小细节小数目这样做。

关于 String.Format 在微软的网站上有一个重要的注释: Https://learn.microsoft.com/en-us/dotnet/api/system.string.format?view=net-6.0

”而不是调用 String。方法或使用复合格式字符串时,如果您的语言支持内插字符串,则可以使用它们。内插字符串是包含内插表达式的字符串。每个内插的表达式都使用表达式的值进行解析,并在分配结果字符串时包含在结果字符串中。”