编译器是做什么的?
让我们从这里开始:
var a = $"Some value 1: {b1:0.00}\n" +
$"Some value 2: {b2}\n" +
$"Some value 3: {b3:0.00000}\n" +
$"Some value 4: {b4:0.00}\n" +
$"Some value 5: {b6:0.0}\n" +
$"Some value 7: {b7:0.000000000}";
IL 对我来说还是个黑盒子
为什么不直接打开它呢?使用 ILSpy、Reflector 等工具非常容易。
在您的代码中将发生的情况是每一行都被编译为string.Format。规则很简单:如果你有$"...{X}...{Y}...",它将被编译为string.Format("...{0}...{1}...", X, Y)。 + 运算符还将引入字符串连接。
更详细地说,string.Format 是一个简单的静态调用,这意味着编译器将使用call 操作码而不是callvirt。
从所有这些你可能会推断出编译器很容易优化它:如果我们有一个像constant string + constant string + ... 这样的表达式,你可以简单地将它替换为constant string。您可以争辩说编译器了解string.Format 的内部工作原理和字符串连接并处理它。另一方面,您可以争辩说不应该。让我详细说明两个注意事项:
请注意,字符串是 .NET 中的对象,但它们是“特殊的”。您可以从以下事实中看出这一点:有一个特殊的 ldstr 操作码,而且如果您检查在字符串上使用 switch 会发生什么——编译器将生成一个字典。因此,由此您可以推断出编译器“知道”string 在内部是如何工作的。让我们弄清楚它是否知道如何进行连接,好吗?
var str = "foo" + "bar";
Console.WriteLine(str);
在 IL(当然是发布模式)中,这将给出:
L_0000: ldstr "foobar"
tl;dr: 所以,无论内插字符串的连接是否已经实现(它们还没有实现),我都非常有信心编译器最终会处理这种情况。
JIT 做什么?
下一个问题是:使用字符串的 JIT 编译器有多聪明?
所以,让我们考虑一下,我们将教编译器了解string 的所有内部工作原理。首先我们要注意C#是编译成IL的,IL是JIT编译成汇编的。对于switch,JIT 编译器很难创建字典,所以我们必须在编译器中完成。另一方面,如果我们正在处理更复杂的连接,那么使用我们已经为 f.ex 提供的东西是有意义的。整数运算也可以进行字符串运算。这意味着将字符串操作放在 JIT 编译器中。让我们用一个例子来考虑一下:
var str = "";
for (int i=0; i<10; ++i) {
str += "foo";
}
Console.WriteLine(str);
编译器将简单地编译连接到 IL,这意味着 IL 将拥有一个非常直接的实现。在这种情况下,循环展开可以说对程序的(运行时)性能有很多好处:它可以简单地展开循环,将字符串附加 10 次,从而产生一个简单的常量。
但是,将这些知识提供给 JIT 编译器会使它变得更加复杂,这意味着运行时将花费更多时间在 JIT 编译(找出优化)上而更少的时间执行(运行发出的汇编程序)。剩下的问题是:会发生什么?
启动程序,在 writeline 上放一个断点,然后按 ctrl-alt-D 并查看汇编程序。
00007FFCC8044413 jmp 00007FFCC804443F
{
str += "foo";
00007FFCC8044415 mov rdx,2BEE2093610h
00007FFCC804441F mov rdx,qword ptr [rdx]
00007FFCC8044422 mov rcx,qword ptr [rbp-18h]
00007FFCC8044426 call 00007FFD26434CC0
[...]
00007FFCC804443A inc eax
00007FFCC804443C mov dword ptr [rbp-0Ch],eax
00007FFCC804443F mov ecx,dword ptr [rbp-0Ch]
00007FFCC8044442 cmp ecx,0Ah
00007FFCC8044445 jl 00007FFCC8044415
tl;dr:不,那不是优化的。
但我也希望 JIT 对其进行优化!
是的,好吧,我不太确定我是否同意这种观点。运行时性能和 JIT 编译所花费的时间之间存在平衡。请注意,如果你在一个紧密的循环中做这样的事情,我认为你是在自找麻烦。另一方面,如果它是一种常见且微不足道的情况(例如连接的常量),则很容易优化并且不会影响运行时。
换句话说:可以说,您不希望 JIT 对此进行优化,假设这会花费太多时间。我相信我们可以相信 Microsoft 会明智地做出这一决定。
此外,您应该意识到 .NET 中的字符串是经过高度优化的东西。我们都知道它们被大量使用,微软也是如此。如果您没有编写“非常愚蠢的代码”,那么这是一个非常合理的假设,即它会执行得很好(除非另有证明)。
替代方案?
分割长插值字符串的其他选项是什么?
使用资源。资源是处理多种语言的有用工具。如果这只是一个小型的非专业项目——我根本不会打扰。
或者,您可以使用连接常量字符串的事实:
var fmt = "Some value 1: {1:0.00}\n" +
"Some value 2: {2}\n" +
"Some value 3: {3:0.00000}\n" +
"Some value 4: {4:0.00}\n" +
"Some value 5: {6:0.0}\n" +
"Some value 7: {7:0.000000000}";
var a = string.Format(fmt, b1, b2, b3, b4, b5, b6, b7);