冗余字符串插值和性能



我一直在我的C#项目中进行一些代码进行重构。我得到了重音代码分析警告:

"冗余字符串插值"

在以下情况下发生这种情况:

void someFunction(string param)
{
...
}
someFunction($"some string");

我已经阅读了字符串插值在编译时重写为string.Format。然后我尝试了以下内容:

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

这次我得到:

冗余字符串。format调用。

我的问题是:除了代码清洁外,是否受这些冗余呼叫影响的运行时性能或性能相同:

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

作为C#编译器中该特定优化的作者,我可以确认$"some string"由C#编译器优化为"some string"。这是一个常数,因此实际上无需在运行时执行代码来计算它。

另一方面,string.Format("some string")是一种方法调用,必须在运行时调用该方法。显然,该呼叫的成本是相关的。当然,它不会做任何有用的事情,因此警告"冗余string.format调用。"

更新:实际上,编译器始终将无填充插入的插值优化为由此产生的字符串。它所做的就是将{{拆除至{}}}。我的更改是优化所有填充插入的插值,而无需格式化字符串串联。

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

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((

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

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

最后,StringBulder必须实例化新的string并在返回池之前复制其内容。

基于 @dmitry的分析,带有"空" FormattableString,编译器可能足够聪明,可以跳过整个过程并仅通过字符串,因为在编译时间中评估了字符串的内容,以将插值的文字转换为FormattableString实例。

我听说字符串插值在编译时间重写为string.Format

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

 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}");

最新更新