冗余字符串插值和性能

Redundant string interpolation and performance

我一直在对我的 C# 项目进行一些代码重构。 我收到 Resharper 代码分析警告:

"Redundant string interpolation"

这发生在以下场景中:

void someFunction(string param)
{
...
}

someFunction($"some string");

我读过字符串插值在编译时被重写为 string.Format。然后我尝试了以下操作:

someFunction(string.Format("some string"));

这次我得到:

Redundant string.Format call.

我的问题是:除了代码整洁度之外,这些冗余调用是否影响 运行 时间性能,或者性能是否相同:

 someFunction($"some string")
 someFunction("some string")  
 someFunction(string.Format("some string"))

即使格式字符串后没有格式参数,调用 string.Format("some string") 也会做很多事情。

由于没有只有一个参数的 string.Format 重载,运行时将首先需要实例化一个空参数数组 (new object[0]) 以将其传递给方法。然后它将从池中获取一个内部 StringBuilder 实例,并开始解析格式字符串,寻找占位符。如果格式字符串中有占位符但没有参数,则会抛出异常,因此该方法始终解析格式字符串。

最后,StringBulder 必须实例化一个新的 string 并将其内容复制到那里,然后再返回到池中。

根据@Dmitry 的分析,使用 "empty" FormattableString,编译器可能足够聪明,可以跳过整个过程并只传递字符串,因为字符串的内容在编译时间将内插文字转换为 FormattableString 实例。

I've heard string interpolation is rewritten to string.Format at compile time

这取决于;如果您的函数改为接受 FormattableString 参数,则编译器将创建一个派生自 FormattableString 的 class 实例,其中包含格式字符串和参数数组。这对于条件调试之类的东西很有用,因为如果不需要,您无需支付格式化字符串的费用,即:

 public void Log_A(string input)
 {
     if (Log.IsDebugEnabled)
         Log.Debug(input);
 }

 public void Log_B(FormattableString input)
 {
     if (Log.IsDebugEnabled)
         Log.Debug(input.ToString());
 }

 // string.Format is called before entering Log_A
 Log_A($"Something happened with {x} and {y}");

 // if Log.IsDebugEnabled is false, string.Format will not be called
 Log_B($"Something happened with {x} and {y}");

好吧,让我们执行一个基准测试:

private static long someFunction(string value) {
  return value.Length;
}

...

Stopwatch sw = new Stopwatch();

int n = 100_000_000;
long sum = 0;

sw.Start();

for (int i = 0; i < n; ++i) {
  // sum += someFunction("some string");
  // sum += someFunction($"some string");
  sum += someFunction(string.Format("some string"));
}

sw.Stop();

Console.Write(sw.ElapsedMilliseconds);

结果(.Net 4.8 IA-64 版本),平均结果:

 224 // "some string"
 225 // $"some string"
8900 // string.Format("some string")

所以我们可以看到,编译器删除了不需要的 $ 但执行了 string.Format 这浪费了时间来理解我们没有任何格式

public void XYZ()
{
    Stopwatch sw = new Stopwatch();

    sw.Start();
    for (int i = 0; i < 1000000; i++)
    {
        someFunction("some string");
    }
    sw.Stop();
    Debug.WriteLine(sw.ElapsedMilliseconds);
    sw.Reset();

    sw.Start();
    for (int i = 0; i < 1000000; i++)
    {
        someFunction("$some string");
    }
    sw.Stop();
    Debug.WriteLine(sw.ElapsedMilliseconds);
    sw.Reset();

    sw.Start();
    for (int i = 0; i < 1000000; i++)
    {
        someFunction(string.Format("some string"));
    }
    sw.Stop();
    Debug.WriteLine(sw.ElapsedMilliseconds);
    sw.Reset();
}

private void someFunction(string param)
{
}

给我

3
3
210

因此,如果不需要,请不要使用 string.Format():-)

作为 C# 编译器中 that specific optimization 的作者,我可以确认 $"some string" 已被 C# 编译器优化为 "some string"。这是一个常量,因此几乎不需要在运行时执行代码来计算它。

另一方面,string.Format("some string")是方法调用,必须在运行时调用该方法。显然,调用会产生相关费用。它当然不会做任何有用的事情,因此警告 "Redundant string.Format call."

更新:事实上,没有填充的插值一直被编译器优化为结果字符串。它所做的只是将 {{ 转义为 { 以及将 }} 转义为 }。我的更改是优化插值,其中所有填充都是字符串,没有格式化为字符串连接。