ASP.NET Core 6 のパフォーマンス向上について

みなさんごきげんよう。本日は以下の記事を紹介いたします。**結構長い**ので、おやつでも食べながらごらんくださいませ。
※ 2022 年 1 月 28 日（米国時間）公開のブログです。

https://devblogs.microsoft.com/dotnet/performance-improvements-in-aspnet-core-6/

-------------------------------

こんにちは！僕 Brennan です。Stephen Toub さんの以下のブログ（uikou 注 : 2021 年の 8 月 17 日公開なのですが…）に触発されたので、自分も ASP.NET Core 6.0 上でのパフォーマンスの向上について記事を書いてアピールしてみようと思ったんです。

https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/

#ベンチマークを設定してみよう
ここでは、[BenchmarkDotNet](https://github.com/dotnet/benchmarkdotnet) を例としてみてみようとおもいます。

https://github.com/dotnet/benchmarkdotnet

ちなみにこの記事で使っているベンチマークの大半については、以下のリポジトリで提供しています。

https://github.com/BrennanConroy/BlogPost60Bench

殆どのベンチマークの結果については、以下のコマンド ラインで生成しています。

```batch
dotnet run -c Release -f net48 --runtimes net48 netcoreapp3.1 net5.0 net6.0
```

次に、リストから実行する特定のベンチマークを選択しましょう。

* すべてをリリース構成で構築
* .NET Framework 4.8 の サーフェス エリアをターゲットに構築
* 各ベンチマークを、.NET Framework 4.8、.NET Core 3.1、.NET 5、および .NET 6 のそれぞれで実行

いくつかのベンチマークでは、.NET 6 でのみ実行しています。(例 : 同じバージョンでコーディングする際に、2 つの方法を比較する場合など）

```bat
dotnet run -c Release -f net6.0 --runtimes net6.0
```

こんな感じで、バージョンのサブセットのみを実行するのもあります。

```bat
dotnet run -c Release -f net5.0 --runtimes net5.0 net6.0
```

これ以降、各ベンチマークの実行時に使ったコマンドについて都度記載していきますね。

この記事で触れていく結果のほとんどは、.NET Framework 4.8 を結果セットに含めるために Windows 上で上記のベンチマークを実行して得たものになります。しかし、今後特に断りがない限りは、一般的にすべてのベンチマークは Linux あるいは macOS 上で実行しても同等の改善を示しました。
測定したいランタイムがインストールされているか、確認してみてください。ベンチマークは、[.NET 6 RC1 の nightly build](https://github.com/dotnet/installer/blob/main/README.md#installers-and-binaries) と、.NET 5 および .NET Core 3.1 の[最新リリース版](https://dotnet.microsoft.com/download)をダウンロードして実行されました。

https://github.com/dotnet/installer/blob/main/README.md#installers-and-binaries

https://dotnet.microsoft.com/download

https://dotnet.microsoft.com/en-us/download/dotnet

#Span\<T>
[.NET 2.1 で Span\<T> が追加](https://docs.microsoft.com/archive/msdn-magazine/2018/january/csharp-all-about-span-exploring-a-new-net-mainstay)されて以来、パフォーマンスの向上のために公開された API、内部 API それぞれにおいてその一部として、多くのコードで Span\<T> を使うように変更を施してきました。本リリースでも同様の対応をしています。

https://docs.microsoft.com/archive/msdn-magazine/2018/january/csharp-all-about-span-exploring-a-new-net-mainstay

Pull request [dotnet/aspnetcore#28855](https://github.com/dotnet/aspnetcore/pull/28855) では、`PathString` のインスタンスを 2 つ追加する際、`string.SubString` 由来の `PathString` のテンポラリ文字列の割り当てを削除し、代わりに `Span<char>` をテンポラリ文字列に使用します。

以下のベンチマークでは、短い文字列と長い文字列を使用して、一時的な文字列の回避によるパフォーマンスの違いを示しています。

```bat
dotnet run -c Release -f net48 --runtimes net48 net5.0 net6.0 --filter *PathStringBenchmark*
```

```csharp
private PathString _first = new PathString("/first/");
private PathString _second = new PathString("/second/");
private PathString _long = new PathString("/longerpathstringtoshowsubstring/");

[Benchmark]
public PathString AddShortString()
{
    return _first.Add(_second);
}

[Benchmark]
public PathString AddLongString()
{
    return _first.Add(_long);
}
```

|Method	|Runtime	  |Toolchain	  |Mean  |Ratio  |Allocated  |
|---|---|---|---|---|---|
|AddShortString	  |.NET Framework 4.8  |net48	  |23.51 ns  |1.00  |96B  |
|AddShortString	 |.NET 5.0  |net5.0  |22.73ns  |0.97  |96B  |
|AddShortString	  |.NET 6.0  |net6.0  |14.92ns  |0.64  |56B  |
||
|AddLongString	|.NET Framework 4.8  |net48  |30.89ns  |1.00  |201B  |
|AddLongString	|.NET 5.0  |net5.0  |25.18ns  |0.82  |192B  |
|AddLongString	 |.NET 6.0  |net6.0  |15.69ns  |0.51  |104B  |

Pull request [dotnet/aspnetcore#34001](https://github.com/dotnet/aspnetcore/pull/34001) では、クエリ文字列を列挙するための新しい `Span` ベースの API が導入されました。この API は、クエリ文字列内にエンコードされた文字が含まれない一般的なケースではアロケーション フリーで、クエリ文字列にエンコードされた文字が含まれている場合には、低めのアロケーションとなります。

https://github.com/dotnet/aspnetcore/pull/34001

```bat
dotnet run -c Release -f net6.0 --runtimes net6.0 --filter *QueryEnumerableBenchmark*
```

```csharp
#if NET6_0_OR_GREATER
    public enum QueryEnum
    {
        Simple = 1,
        Encoded,
    }

    [ParamsAllValues]
    public QueryEnum QueryParam { get; set; }

    private string SimpleQueryString = "?key1=value1&key2=value2";
    private string QueryStringWithEncoding = "?key1=valu%20&key2=value%20";

    [Benchmark(Baseline  = true)]
    public void QueryHelper()
    {
        var queryString = QueryParam == QueryEnum.Simple ? SimpleQueryString : QueryStringWithEncoding;
        foreach (var queryParam in QueryHelpers.ParseQuery(queryString))
        {
            _ = queryParam.Key;
            _ = queryParam.Value;
        }
    }

    [Benchmark]
    public void QueryEnumerable()
    {
        var queryString = QueryParam == QueryEnum.Simple ? SimpleQueryString : QueryStringWithEncoding;
        foreach (var queryParam in new QueryStringEnumerable(queryString))
        {
            _ = queryParam.DecodeName();
            _ = queryParam.DecodeValue();
        }
    }
#endif
```

|Method	|QueryParam|Mean  |Ratio  |Allocated  |
|---|---|---|---|---|---|
|QueryHelper|Simple |243.13ns|1.00  |360B  |
|QueryEnumerable|Simple|91.43ns  |0.38  |-  |
||
|QueryHelper|Encoded|351.25ns  |1.00  |432B  |
|QueryEnumerable|Encoded|197.59ns  |0.56  |152B|

ここで注意したいのは、[何もせずに何かを得られることはない](https://ja.wikipedia.org/wiki/TANSTAAFL)ということです。新しい `QueryStringEnumerable` API のケースでは、クエリ文字列の値を複数回列挙するつもりの場合、実際には `QueryHelpers.ParseQuery` により解析したクエリ文字列値のディクショナリに保存するよりもコストが高くつく可能性があります。

[@paulomorgado](https://github.com/paulomorgado) による [dotnet/aspnetcore#29448](https://github.com/dotnet/aspnetcore/pull/29448) では、[string.Create](https://docs.microsoft.com/dotnet/api/system.string.create) メソッドを使用すると、文字列の最終的なサイズがわかっていれば、作成後に文字列を初期化することができます。これにより、`UriHelper.BuildAbsolute` でのテンポラリ文字列の割り当てを削除しました。

https://github.com/dotnet/aspnetcore/pull/29448

```bat
dotnet run -c Release -f netcoreapp3.1 --runtimes netcoreapp3.1 net6.0 --filter *UriHelperBenchmark*
```

```csharp
#if NETCOREAPP
    [Benchmark]
    public void BuildAbsolute()
    {
        _ = UriHelper.BuildAbsolute("https", new HostString("localhost"));
    }
#endif
```

|Method	|Runtime	  |Toolchain	  |Mean  |Ratio  |Allocated  |
|---|---|---|---|---|---|
|BuildAbsolute|.NET Core 3.1 |netcoreapp3.1	  |92.87 ns  |1.00  |176B  |
|BuildAbsolute|.NET 6.0  |net6.0  |52.88ns  |0.57  |64B  |

Pull request [dotnet/aspnetcore#31267](https://github.com/dotnet/aspnetcore/pull/31267) は、`ContentDispositionHeaderValue` の一部の解析ロジックを `Span<T>` ベースの API に変換し、一般的なケースでテンポラリ文字列やテンポラリの `byte[]` を避けるように実装しました。

```bat
dotnet run -c Release -f net48 --runtimes net48 netcoreapp3.1 net5.0 net6.0 --filter *ContentDispositionBenchmark*
```

```csharp
[Benchmark]
public void ParseContentDispositionHeader()
{
    var contentDisposition = new ContentDispositionHeaderValue("inline");
    contentDisposition.FileName = "FileÃName.bat";
}
```

|Method	|Runtime	  |Toolchain	  |Mean  |Ratio  |Allocated  |
|---|---|---|---|---|---|
|ContentDispositionHeader|.NET Framework 4.8  |net48	  |654.9 ns  |1.00  |570B  |
|ContentDispositionHeader|.NET Core 3.1  |netcoreapp3.1|581.5 ns  |0.89  |536B  |
|ContentDispositionHeader|.NET 5.0  |net5.0  |519.2 ns  |0.79  |536B  |
|ContentDispositionHeader|.NET 6.0  |net6.0  |295.4 ns  |0.45  |312B  |

#アイドル接続関連の改善
ASP.NET Core の主要コンポーネントの 1 つであるサーバーのホスティングには、最適化すべきさまざまな問題があります。
このトピックでは、6.0 のアイドル接続の改善にフォーカスしたいと思います。.NET 6.0 では、コネクションのデータ待ちの間の使用メモリ量を減らすために多くの変更を行いました。

その 1 つが、コネクションで使用されるオブジェクトのサイズを小さくすることです。これには `System.IO.Pipelines`、`SocketConnections`、および `SocketSender` が含まれます。
2 つ目は、一般的にアクセスされるオブジェクトをプールすること、つまり古いインスタンスを再利用して割り当てを節約することです。
3 つ目は、「Zero byte reads」と呼ばれる方法の利用です。利用可能なデータがある場合、読み取りはデータなしで返されますが、利用可能なデータがあることがわかれば、そのデータをすぐに読み取るためのバッファを提供することができます。これにより、将来完了するかもしれない読み込みのために、前もってバッファを確保することを避け、データが利用可能であることがわかるまで、大きな割り当てを避けることができます。

[dotnet/runtime#49270](https://github.com/dotnet/runtime/pull/49270) は、`System.IO.Pipelines` のサイズを ~560 バイトから ~368 バイトまで、34% 削減しました。 

https://github.com/dotnet/runtime/pull/49270

[dotnet/aspnetcore#31308](https://github.com/dotnet/aspnetcore/pull/31308) は、Kestrel の Socket レイヤをリファクタリングし、いくつかの非同期ステート マシンを回避し、残りのステート マシンのサイズを縮小することで、各接続の割り当てを ~33% 削減しました。

https://github.com/dotnet/aspnetcore/pull/31308

[dotnet/aspnetcore#30769](https://github.com/dotnet/aspnetcore/pull/30769) は、接続ごとの `PipeOptions` の割り当てを削除し、割り当てを接続ファクトリに移しました。 

https://github.com/dotnet/aspnetcore/pull/30769

[@benaadams](https://github.com/benaadams) による、[@dotnet/aspnetcore#31311](https://github.com/dotnet/aspnetcore/pull/31311)では、`WebSocket` リクエストの Well-known ヘッダ値をインターン化文字列に置換。これにより、ヘッダ解析時に割り当てられた文字列をガベージ コレクションすることで、長期間使用される `WebSocket` 接続のメモリ使用量を削減しました。

https://github.com/dotnet/aspnetcore/pull/31311

[dotnet/aspnetcore#30771](https://github.com/dotnet/aspnetcore/pull/30771) は、Kestrel のソケット レイヤをリファクタリングし、まず `SocketReceiver` オブジェクトと `SocketAwaitableEventArgs` の割り当てを避け、単一オブジェクトに統合し、数バイトが節約されました。
さらに接続ごとに割り当てられるユニークなオブジェクトを減らしました。また、この Pull request では `SocketSender` クラスをプールするため、接続ごとに 1 つ作成するのではなく、平均してコア数の SocketSender を作成できるようになりました。
したがって、以下のベンチマークでは、接続数 10,000 の場合、私のマシン上での割り当ては 10,000 ではなく 16 だけが割り当てられてることになります。これにより、約 46MB を節約することができました。

https://github.com/dotnet/aspnetcore/pull/30771

[dotnet/runtime#49123](https://github.com/dotnet/runtime/pull/49123) では、`SslStream` での Zero byte reads のサポートが追加され、10,000 のアイドル接続が `SslStream` の割り当てにより、約 46 MB から約 2.3 MB のサイズになりました。

こうした多くの変更の集大成として、アイドル接続時のメモリ使用量の大幅削減を達成することに成功しました。

以下の結果は、BenchmarkDotNet アプリではアイドル接続を測定しているため、クライアントとサーバのアプリケーションでセットアップする方が簡単でした。コンソールと WebApplication のコードは以下の gist にあります。

https://gist.github.com/BrennanConroy/02e8459d63305b4acaa0a021686f54c7

下記の表は、以下のさまざまなフレームワークにおいて、アイドル状態の 10,000 本のセキュアな WebSocket 接続（WSS）がサーバ上で使用しているメモリ量です。

|Framework|Memory	  |
|---|---|
|net48|665.4MB|
|net5.0|603.1MB	  |
|net6.0	|160.8MB	  |

.NET 5.0 から 6.0 への移行によって、メモリ使用量は約 4 倍削減されました。

#Entity Framework Core (EF Core)
EF Core は 6.0 において大規模な改善を行いました。ランタイムのアップデート、ベンチマークの最適化、そして Entity Framework 自体の改善により、クエリの実行が 31% 高速化、さらに [TechEmpower Fortunes ベンチマーク](https://www.techempower.com/benchmarks/#section=data-r20)は 70% 改善されました。

https://www.techempower.com/benchmarks/#section=data-r20

こうした改善は、オブジェクト プーリングの改善、テレメトリが有効になっているかどうかのインテリジェントなチェック、アプリケーションが `DbContext` を安全に使用していることがわかっている場合、スレッド セーフのチェックをオプトアウトするオプションの追加などのたまものです。

改善点の詳細については、ブログ [Announcing Entity Framework Core 6.0 Preview 4: Performance Edition](https://devblogs.microsoft.com/dotnet/announcing-entity-framework-core-6-0-preview-4-performance-edition/) （※英語）をご覧ください。

https://devblogs.microsoft.com/dotnet/announcing-entity-framework-core-6-0-preview-4-performance-edition/

#Blazor
##ネイティブの`byte[]` 相互運用について
Blazor は、JavaScript の相互運用を行う際に、バイト配列を効率的にサポートするようになりました。
以前は、JavaScript との間で送受信されるバイト配列は、JSON としてシリアライズできるように Base64 エンコードされていたため、転送サイズと CPU負荷が増大することになりました。.NET 6 では Base64 エンコーディングの処理が最適化されることで、.NET では `byte[]`、JavaScript では `Uint8Array` を透過的に扱うことができるようになりました。JavaScript →.NET、.NET → JavaScript の状況において、この機能に関するドキュメントは以下を参照ください。

https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-6.0#byte-array-support

さてここで、.NET 5 と .NET 6 の `byte[]` 相互運用の違いを見るために、簡単なベンチマークを見てみたいと思います。
次の Razor コードは、22 KB の `byte[]` を作成し、JavaScript の `receiveAndReturnBytes` 関数に送り、即座に `byte[]` を返しています。このデータの往復は 10,000 回繰り返され、その時のデータが画面に表示されます。
このコードは、.NET 5 でも .NET 6 でも同じものを使用しています。

```csharp
<button @onclick="@RoundtripData">Roundtrip Data</button>

<hr />

@Message

@code {
    public string Message { get; set; } = "Press button to benchmark";

    private async Task RoundtripData()
    {
        var bytes = new byte[1024*22];
        List<double> timeForInterop = new List<double>();
        var testTime = DateTime.Now;

        for (var i = 0; i < 10_000; i++)
        {
            var interopTime = DateTime.Now;

            var result = await JSRuntime.InvokeAsync<byte[]>("receiveAndReturnBytes", bytes);

            timeForInterop.Add(DateTime.Now.Subtract(interopTime).TotalMilliseconds);
        }

        Message = $"Round-tripped: {bytes.Length / 1024d} kB 10,000 times and it took on average {timeForInterop.Average():F3}ms, and in total {DateTime.Now.Subtract(testTime).TotalMilliseconds:F1}ms";
    }
}
```

次に、JavaScript の `receiveAndReturnBytes` 関数を見てみます。.NET 5.0 では まず、Base64 エンコードされたバイト配列を `Uint8Array` にデコードして、アプリケーション コードで使用できるようにする必要があります。その次に、データをサーバに返す前に、Base64 に再エンコードする必要があります。

```csharp
function receiveAndReturnBytes(bytesReceivedBase64Encoded) {
    const bytesReceived = base64ToArrayBuffer(bytesReceivedBase64Encoded);

    // Use Uint8Array data in application

    const bytesToSendBase64Encoded = base64EncodeByteArray(bytesReceived);

    if (bytesReceivedBase64Encoded != bytesToSendBase64Encoded) {
        throw new Error("Expected input/output to match.")
    }

    return bytesToSendBase64Encoded;
}

// https://stackoverflow.com/a/21797381
function base64ToArrayBuffer(base64) {
    const binaryString = atob(base64);
    const length = binaryString.length;
    const result = new Uint8Array(length);
    for (let i = 0; i < length; i++) {
        result[i] = binaryString.charCodeAt(i);
    }
    return result;
}

function base64EncodeByteArray(data) {
    const charBytes = new Array(data.length);
    for (var i = 0; i < data.length; i++) {
        charBytes[i] = String.fromCharCode(data[i]);
    }
    const dataBase64Encoded = btoa(charBytes.join(''));
    return dataBase64Encoded;
}
```

このエンコード / デコード処理は、クライアント、サーバ双方に大きなオーバーヘッドをもたらすだけでなく、大規模なボイラープレート コードも必要です。では、.NET 6 ではどのようにすればよいでしょうか？
…実は、とてもシンプルになるのです。

```csharp
function receiveAndReturnBytes(bytesReceived) {
    // bytesReceived comes as a Uint8Array ready for use
    // and can be used by the application or immediately returned.
    return bytesReceived;
}
```

確かに書くのは簡単なようですが…パフォーマンスはどうでしょうか？
これらのスニペットを `blazorserver` テンプレートで .NET 5 と .NET 6 それぞれで Release 構成で実行してみます。
すると、.NET 6 では byte[] の相互運用パフォーマンスが 78% 向上していることがわかるかと思います。

|—————|.NET 6(ms)  |.NET 5(ms)  |Improvement  |
|---|---|---|---|
|Total time|5,273|24,463|78%|

さらに、バイト配列の相互運用サポートは、フレームワーク内で活用され、JavaScript と .NET 間で双方向のストリーミング相互運用を可能にします。ユーザーは任意のバイナリ データを転送できるようになりました。
.NET から JavaScript へのストリーミングに関するドキュメントは以下をご参照ください。

https://docs.microsoft.com/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-6.0#stream-from-net-to-javascript

##Input File
さて、前述の Blazor のストリーミングの相互運用を活用し、`InputFile` コンポーネントによる[大容量ファイルのアップロードがサポート](https://docs.microsoft.com/aspnet/core/blazor/file-uploads?view=aspnetcore-6.0)されました（以前はアップロードが 2GB 程度に制限されていました）。

https://docs.microsoft.com/aspnet/core/blazor/file-uploads?view=aspnetcore-6.0

このコンポーネントでは、Base64 エンコーディングを経由せず、ネイティブの `byte[]` ストリーミングを使用することで、大幅に速度が向上されました。例えば、100MB のファイルをアップロードする場合、.NET 5 と比較して 77% の高速化が実現されました。

|.NET 6(ms)  |.NET 5(ms)  |Percentage|
|---|---|---|
|2,591|10,504|75%|
|2,607|11,764|78%|
|2,632|11,821|78%|
||平均 : |77%|

ストリーミング相互運用のサポートにより、（大容量の）ファイルの効率的なダウンロードも可能になっています。詳細については、以下のドキュメントをご参照ください。

https://docs.microsoft.com/aspnet/core/blazor/file-downloads?view=aspnetcore-6.0

`InputFile` コンポーネントは、[dotnet/aspnetcore#33900](https://github.com/dotnet/aspnetcore/pull/33900) により、ストリーミングを利用するようにアップグレードされました。

https://github.com/dotnet/aspnetcore/pull/33900

#その他いろいろ
[@benaadams](https://github.com/benaadams) さんによる、dotnet/aspnetcore#30320 (https://github.com/dotnet/aspnetcore/pull/30320) は、ウェブサイトの読み込みが速くなるように、Typescript ライブラリをモダナイズし、最適化しました。`signalr.min.js` ファイルは、圧縮時に 36.8 KB、非圧縮時に 132 KB だったのが、圧縮時に 16.1 KB、非圧縮時に 42.2 KB になりました。
また、`blazor.server.js` ファイルは、86.7 KB の圧縮ファイルと 276 KB の非圧縮ファイルが、43.9 KB の圧縮ファイルと 130 KB の非圧縮ファイルになります。

https://github.com/dotnet/aspnetcore/pull/30320

同じく、[@benaadams](https://github.com/benaadams) さんによる、dotnet/aspnetcore#31322 (https://github.com/dotnet/aspnetcore/pull/31322) では、接続機能コレクションから共通機能を取得する際、いくつかの不要なキャストを削除しました。これにより、コレクションから共通機能にアクセスする際のパフォーマンスが約 50% 向上しました。
残念ながら、たくさんのインターナルの型が必要になるため、ベンチマークで性能向上を確認することは残念ながらできません。そこで、Pull request に掲載されている数値をここに掲載しておきたいと思います。この Pull request にはインターナルのコードに対して実行可能なベンチマークが含まれていますので、もしご興味があればこちらを見てみてください。 

https://github.com/dotnet/aspnetcore/pull/31322

|Method|Mean  |Op/s  |Diff|
|---|---|---|---|
|`Get<IHttpRequestFeature>*`|8.507 ns|117,554,189.6|+50.0%|
|`Get<IHttpResponseFeature>*`|9.034 ns|110,689,963.7|--|
|`Get<IHttpResponseBodyFeature>*`|9.466 ns|105,636,431.7|+58.7%|
|`Get<IRouteValuesFeature>*`|10.007 ns|99,927,927.4|+50.0%|
|`Get<IEndpointFeature>*`|10.564 ns|94,656,794.2|+44.7%|

またまた [@benaadams](https://github.com/benaadams) さんによる、[dotnet/aspnetcore#31519] (https://github.com/dotnet/aspnetcore/pull/31519) は、ヘッダ名にちなんだプロパティを使用し、一般的なヘッダにアクセスするための既定のインタフェース メソッドを `IHeaderDictionary` 型に追加しています。これによりヘッダの辞書にアクセスする際、共通のヘッダを間違えなくて済むようになります。興味深いことに、この変更により、サーバの実装において、これら新インタフェース・メソッドをより最適に実装したカスタム ヘッダ辞書を返せるようになりました。例えば、キーをハッシュ化してエントリを検索する必要のある内部辞書にヘッダ値を問い合わせる代わりに、サーバはヘッダ値をフィールドに直接格納して、そのフィールドを直接返すことが可能です。
この変更により、ヘッダ値を取得または設定する際の処理を最大で 480% 改善することができました。以下、Pull request の数値となります。

|Method	|Branch|Type|Mean  |Op/s|Delta  |
|---|---|---|---|---|---|
|GetHeaders|before |Plaintext|25.793 ns  |38,770,569.6|-|
|GetHeaders|after |Plaintext|12.775 ns  |78,279,480.0|+101.9%|
|GetHeaders|before |Common|121.355 ns|8,240,299.3|-|
|GetHeaders|after |Common|37.598 ns|26,597,474.6|+222.8%|
|GetHeaders|before |Unknown|366.456 ns|2,728,840.7|-|
|GetHeaders|after |Unknown|223.472 ns|4,474,824.0|+64.0%|
||
|SetHeaders|before |Plaintext|49.324 ns  |20,273,931.8|-|
|SetHeaders|after |Plaintext|34.996 ns|28,574,778.8|+40.9%|
|SetHeaders|before |Common|635.060 ns|1,574,654.3|-|
|SetHeaders|after |Common|108.041 ns|9,255,723.7|+487.7%|
|SetHeaders|before |Unknown|1,439.945 ns|694,470.8|-|
|SetHeaders|after |Unknown|517.067 ns|1,933,985.7|+178.4%|

[dotnet/aspnetcore#31466](https://github.com/dotnet/aspnetcore/pull/31466) は、.NET 6 で導入された新しい `CancellationTokenSource.TryReset` 関数を使用して、接続がキャンセルされずに閉じた場合に `CancellationTokenSource` を再利用するようにしました。以下の数字は、Kestrel に対して 125 本の接続で [bombardier](https://github.com/codesenberg/bombardier) を実行し、約 100,000 のリクエストを実行し、測定したものです。

https://github.com/dotnet/aspnetcore/pull/31466

https://github.com/codesenberg/bombardier

|Branch  |Type  |Allocations|Bytes|
|---|---|---|---|
|Before|CancellationTokenSource|98,314|	4,719,072|
|After|CancellationTokenSource|125|6,000|

[dotnet/aspnetcore#31528](https://github.com/dotnet/aspnetcore/pull/31528) および [dotnet/aspnetcore#34075](https://github.com/dotnet/aspnetcore/pull/34075) は、HTTPS ハンドシェイクおよび HTTP3 ストリーム用の `CancellationTokenSource` の再利用について、それぞれ同様の変更を施しました。

https://github.com/dotnet/aspnetcore/pull/31528

https://github.com/dotnet/aspnetcore/pull/34075

[dotnet/aspnetcore#31660](https://github.com/dotnet/aspnetcore/pull/31660) は、ストリーム　アイテムごとに 1 割り当てるのではなく、そのかわりに、ストリーム全体に割り当てられた `StreamItem` オブジェクトを再利用することによって、SignalR での[サーバーからクライアントへのストリーミングの性能を改善しました](https://docs.microsoft.com/aspnet/core/signalr/streaming?view=aspnetcore-5.0#server-to-client-streaming)。また、[dotnet/aspnetcore#31661](https://github.com/dotnet/aspnetcore/pull/31661)では、`HubCallerClients` オブジェクトを Hub のメソッド呼び出しごとに割り当てるのではなく、SignalR の接続に格納します。

https://github.com/dotnet/aspnetcore/pull/31660

https://docs.microsoft.com/aspnet/core/signalr/streaming?view=aspnetcore-5.0#server-to-client-streaming

https://github.com/dotnet/aspnetcore/pull/31661

[@ShreyasJejurkar](https://github.com/ShreyasJejurkar) さんによる、[dotnet/aspnetcore#31506](https://github.com/dotnet/aspnetcore/pull/31506) は、テンポラリの List\<T> の割り当てを避けるために `WebSocket` ハンドシェイクの内部をリファクタリングしました。

https://github.com/dotnet/aspnetcore/pull/31506

[@benaadams](https://github.com/benaadams) さんの [dotnet/aspnetcore#32234](https://github.com/dotnet/aspnetcore/pull/32829) は、`HttpRequestHeaders` の列挙で使用されていないフィールドを削除しました。これにより、列挙されたすべてのヘッダ フィールドへの割り当てがなくなり、結果としてパフォーマンスが向上しました。

https://github.com/dotnet/aspnetcore/pull/32829

[@martincostello](https://github.com/martincostello) さんからの [dotnet/aspnetcore#31333](https://github.com/dotnet/aspnetcore/pull/31333) では、`Http.Sys` を高性能ログ API である `LoggerMessage.Define` を使用するように変換しました。これにより、Value 型の不必要なボックス化、ログ フォーマット文字列の解析および場合によっては、ログレベルが有効でないときの文字列またはオブジェクトの割り当てを回避することが可能です。

https://github.com/dotnet/aspnetcore/pull/31333

[dotnet/aspnetcore#31784](https://github.com/dotnet/aspnetcore/pull/31784) では、ミドルウェアを登録するため、新しく `IApplicationBuilder.Use` オーバーロードが追加され、ミドルウェア実行時の不要なリクエストごとの割り当てを回避します。

旧コード :

```csharp
app.Use(async (context, next) =>
{
    await next();
});
```

新コード : 

```csharp
app.Use(async (context, next) =>
{
    await next(context);
});
```

https://github.com/dotnet/aspnetcore/pull/31784

以下のベンチマークでは、改善点を示すためにサーバ設置せずにミドルウェアのパイプラインをシミュレートしています。
リクエストには `HttpContextの` 代わりに `int` が使用され、ミドルウェアは完了したタスクを返します。

```bat
dotnet run -c Release -f net6.0 --runtimes net6.0 --filter *UseMiddlewareBenchmark*
```

```csharp
static private Func<Func<int, Task>, Func<int, Task>> UseOld(Func<int, Func<Task>, Task> middleware)
{
    return next =>
    {
        return context =>
        {
            Func<Task> simpleNext = () => next(context);
            return middleware(context, simpleNext);
        };
    };
}

static private Func<Func<int, Task>, Func<int, Task>> UseNew(Func<int, Func<int, Task>, Task> middleware)
{
    return next => context => middleware(context, next);
}

Func<int, Task> Middleware = UseOld((c, n) => n())(i => Task.CompletedTask);
Func<int, Task> NewMiddleware = UseNew((c, n) => n(c))(i => Task.CompletedTask);

[Benchmark(Baseline = true)]
public Task Use()
{
    return Middleware(10);
}

[Benchmark]
public Task UseNew()
{
    return NewMiddleware(10);
}
```

|Method|Mean  |Ratio  |Allocated|
|---|---|---|---|
|`Use`|15.832 ns|1.00|96B|
|`UseNew`|2.592 ns|0.16|--|

#まとめ
ASP.NET Core 6.0 で改善されたポイントについて、お楽しみいただけたでしょうか。
ランタイムのパフォーマンスについては、Stephen Toub さんの以下のブログをぜひ併せてご覧ください。

https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/

-----------------

以上、[Brennan Conroy](https://devblogs.microsoft.com/dotnet/performance-improvements-in-aspnet-core-6/) さんでした。

![img](https://devblogs.microsoft.com/dotnet/wp-content/uploads/sites/10/2022/01/4ef6553a-33d1-4326-8d8c-1156c6509162-150x150.jpg)

それではみなさんごきげんよう。