.NET 6(c#10)から文字列補完の最適化が入り、以前までコンパイラはString.Formatとして解釈していたものを、ボックス化のコストを削減・無駄にStringを生成しないDefaultInterporatedStringHandlerに置き換えています。
最適化方法に興味があり実装などを調べてみたら面白かったので簡単にまとめておきます。

※ 本ポストに記載しているプログラムはLINQPad 8.NET 8環境で動作したものを利用しています。

String.Formatの問題点

そもそもなぜ、文字列補完に最適化の必要があったのか。
文字列補完は元々String.Formatとして解釈されていました。
String.Formatはボックス化を伴う実装となっていてマネージドヒープにゴミが残ってしまいます。

public partial class Program {
  public static void Main() {
    int a = 100;
    int b = 200;
    int c = 300;
    // 第2引数以降はobject
    Console.WriteLine(string.Format("{0}-{1}-{2}", a, b, c));
  }
}

ILを見るとより分かりやすいです。
引数に渡すすべての数値に対し、box命令によるボックス化が発生しています。

これは非常に効率が悪いです。

DefaultInterpolatedStringHandlerを利用した実装

先ほどのプログラムを文字列補完で実装し、再度ILを見てみるとDefaultInterporatedStringHandlerが利用されていることが分かります。

public partial class Program {
  public static void Main() {
    int a = 100;
    int b = 200;
    int c = 300;
    // 文字列補完
    Console.WriteLine($"{a}-{b}-{c}");
  }
}

このILをC#にすると👇のような形になる。

public partial class Program
{
  public static void Main()
  {
    int a = 100;
    int b = 200;
    int c = 300;
    
    var handler = new DefaultInterpolatedStringHandler(2, 3);
    handler.AppendFormatted(a);
    handler.AppendLiteral("-");
    handler.AppendFormatted(b);
    handler.AppendLiteral("-");
    handler.AppendFormatted(c);
    Console.WriteLine(handler.ToStringAndClear());
  }
}

これを見て私は「もしAppendFormattedの内部でToStringしていたら結局パフォーマンス悪くない?」と疑問に思ったわけです。
そこで実装を見てきたので👇にまとめておきます。

コンストラクタの挙動

コンストラクタにはリテラルの文字数と、動的に挿入される変数の数を指定する。
DefaultInterpolatedStringHandlerArrayPool<char>.Shared.Rentメソッドを利用してバッファを確保する。
この時バッファのサイズはリテラルの文字数 * 変数の数 * 11256を比較し大きい方の分だけメモリを確保する。
確保したバッファは内部のSpan<char> _charsに保持される。

AppendFormattedメソッドの挙動

引数がIFormattableISpanFomattableを実装している場合、TryFormatメソッドを呼び出し、バッファに対して文字を書き込む。
TryFormatは低レベルな実装となっており、内部でToStringされることは基本的にない。
また、バッファのサイズが足りずにバッファへの書き込みに失敗した場合、さらに大きいバッファを確保しなおして再度書き込み処理を実行する。
ISpanFormattableを実装していない場合ToStringを呼び出し、Stringの文字列をバッファに書き込む。
つまり、DefaultInterpolatedStringHandlerの恩恵を100%受けるにはIFormattableISpanFomattableの実装が必要みたいですね。
Int32.TryFormat等の実装を見ると面白いかも。

AppendLiteralメソッドの挙動

こちらはAppendFormattedメソッドと比べると簡単で、引数の文字列をバッファにコピーする。
AppendFormattedと同様にバッファのサイズが足りない場合、バッファを再確保して文字列を書き込む。

ToStringAndClearメソッドの挙動

new String(ReadonlySpan<char>)を呼び出し、Stringを生成し、文字列を返却する。
この時ArrayPool<char>.Shared.Returnメソッドを利用してバッファをプールに返還する。

気を付けなければいけないのがDefaultInterpolatedStirngHandler.ToStringだとArrayPool<char>.Shared.Returnを呼び出す処理は実行されない。

まとめ

StringBuilderの代わりにもなるので、とりあえずDefaultInterpolatedStringHandlerをガンガン使いたい、ところではありますが、絶妙に利用しづらいAPIな上にToStringAndClearを利用しないと事故る可能性があるので、何とも言えない。
(一定以上の理解度の人間がそろってないと事故りそう)
直接利用するは個人開発のアプリやツールにとどめ、業務ではコンパイラに全てを委ねるかぁと思いました。