💪

【.NET】直近でマージされたPRの紹介 その2

に公開

はじめに

PR Digest.NET[1]で毎日、dotnet/runtimeでのマージされたPRをふわっと観測しているとたまに「なるほど!」と思うPRと出会うことがあります。
ということで、今回は比較的直近にマージされたPRの中で、個人的に印象がのこったPRや「そんなバグあったのか?」というPRを独断と偏見でまとめました。
※直近かどうかはあやしいですが、上げているPRは2026年にマージされたものなので、直近とさせてください。

前回は以下になります。
https://zenn.dev/prozolic/articles/2d3c2d50bb8c15

確認環境

一部動作の再現確認には、記事を投稿した時点で正式リリース前の最新プレビュー版(.NET 11 preview2)またはdotnet/runtimeのローカルビルドランタイムでデバッグして確認しています。

[#122779] Intrinsify Enum.Equals to avoid boxing

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

Enum.Equalsに対して、JITの特殊対応(JIT intrinsic)が入りました。
どういう変更があったのかは、以下のようなサンプルコードで確認します。

サンプルコード
// こんな感じで実行する
var check = EnumTest.IsEnumEquals(StringComparison.Ordinal, StringComparison.OrdinalIgnoreCase);
Console.WriteLine(check);

public static class EnumTest
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsEnumEquals<T>(T x, T y) where T : struct, Enum
    {
        return x.Equals(y);
    }
}

.NET10やそれ以前のランタイムでは、上記ケースではEnum.Equalsにてボクシングが発生するアセンブリが出力されます。
出力内容をみても、各引数のStringComparisonはCORINFO_HELP_NEWSFASTによってヒープに割り当てられた後に、Enum.Equals(object)を使って比較を行っています。
確かにEnum自体にはEquals(object)で実装されているため、仕方ないという気もしますが。

NET10でのEnum.Equals
G_M000_IG01:
       push     rdi
       push     rsi
       push     rbp
       push     rbx
       sub      rsp, 40
       mov      ebx, ecx
       mov      esi, edx
 
G_M000_IG02:
       mov      rdi, 0xD1FFAB1E
       mov      rcx, rdi
       call     CORINFO_HELP_NEWSFAST      ;ヒープへ割り当てられている
       mov      rbp, rax
       mov      dword ptr [rbp+0x08], esi
       mov      rcx, rdi
       call     CORINFO_HELP_NEWSFAST      ;ヒープへ割り当てられている
       mov      dword ptr [rax+0x08], ebx
       mov      rcx, rax
       mov      rdx, rbp
       call     [System.Enum:Equals(System.Object):bool:this]
       nop      
 
G_M000_IG03:
       add      rsp, 40
       pop      rbx
       pop      rbp
       pop      rsi
       pop      rdi
       ret      
 
; Total bytes of code 69

.NET11では、以下のように各StringComparisonを数値として直接比較するcmp命令に変更されて、実にシンプルな出力になりました!

NET11
G_M000_IG01:
 
G_M000_IG02:
       cmp      ecx, edx      ;直接StringComparisonの数値で比較!
       sete     al
       movzx    rax, al
 
G_M000_IG03:
       ret      
 
; Total bytes of code 9

実装としては、まずライブラリではEnum.Equal(object)[Intrinsic]属性が適用されて、JITの特殊対応の仲間入りをしました。
https://github.com/dotnet/runtime/blob/v11.0.100-preview.2.26159.112/src/libraries/System.Private.CoreLib/src/System/Enum.cs#L1190-L1193

JIT側では、Enum.HasFlag[2]で行われた最適化対応とほぼ同じような対応が実装されました。
実装部分としては、src/coreclr/gentree.cppCompiler::gtFoldExprCallにてNI_System_Enum_Equals分岐処理が追加されています。

https://github.com/dotnet/runtime/blob/v11.0.100-preview.2.26159.112/src/coreclr/jit/gentree.cpp#L13922-L13964

ここでは、引数のEnumがfloatとdouble以外の数値型であるかなどのチェックを行い、今回の最適化対象である場合には、数値として直接比較するcmp命令に変換されてます。

StringComparisonから直接値を取り出し、数値として比較するcmp命令に変換している
 // Unbox both integral arguments and compare their underlying values
 GenTree* offset  = gtNewIconNode(TARGET_POINTER_SIZE, TYP_I_IMPL);
 GenTree* addr0   = gtNewOperNode(GT_ADD, TYP_BYREF, arg0, offset);
 GenTree* addr1   = gtNewOperNode(GT_ADD, TYP_BYREF, arg1, gtCloneExpr(offset));
 GenTree* cmpNode = gtNewOperNode(GT_EQ, TYP_INT, gtNewIndir(typ, addr0), gtNewIndir(typ, addr1));
 JITDUMP("Optimized Enum.Equals call to comparison of underlying values:\n");
 DISPTREE(cmpNode);
 JITDUMP("\n");

[#124475] Build zstd with /source-charset:utf-8 option

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

このPRは不具合修正というより設定変更になりますが、個人的に記憶に残っていたので、ピックアップしました。
内容としては、zstd_compress.cにて非ASCII文字が含まれていた、かつWindowsの日本語環境ではソースをCP932(Shift-JIS)で読み込んでしまったため、ビルドできなくなったというものです。

記憶に残っている理由としては、私もこの現象に遭遇してビルドできなくなりましたので、よく覚えていました。
私の環境では、ビルドが失敗する原因であるzstd_compress.cをUTF-8(BOM無し)からUTF-8(BOM有り)に変更するという強引な方法で解決していましたが、PRではソース変更ではなくCMakeのソース文字セットをUTF-8に設定する方法(/source-charset:utf-8)でマージされました。

[#124571] Eliminate redundant bounds checks for arr[^N-1] after arr[^N]

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

ReadOnlySpan<T>で末尾から取得するIndexパターン(例:[^4])において、不要な境界チェックを削除するように修正したPRになります。

再現コード
public static class Test
{
    public static int DecodeRemaining(ReadOnlySpan<int> source)
    => source[^4] | source[^3] | source[^2] | source[^1];
}

対応前では、source[^4]source[^3]source[^2]source[^1]の計4回の境界値チェックが実行されていました。
ただ、source[^4]source[source.Length - 4]であり、これが正常に取得できれば、source.Length >= 4となるため、source[^3]source[^2]source[^1]も正常に取得できることになるため、3回分の境界値チェックは不要になります。
なのに、source[^3]source[^2]source[^1]でも境界値チェックが行われていたため、このPRで境界値チェックを行わないようになりました。

個人的にはこのパターンを使うケースがすぐに浮かばないのですが、.NETのコンパイラ開発者のEgorさんがIssue[3]に出されていたので、意外に存在するケースということにしました。

[#124555] Remove Windows 7 support code from System.Net.Security

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

System.Net.Securityのメソッドで参照しているWindows7固有のソースを削除しているPRになります。
Windows7は2020年1月14日にサポートが終了していますが、意外にもまだWindows7の固有のコードが残っていたことに驚いたので追加しました。

[#124737] Remove duplicate typeof(decimal) check in IsKnownComparable

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

私が最近出したPRになりますが、中身は一番大したことない修正になります。
System.Collections.ImmutableプロジェクトのConstants.IsKnownComparable<T>メソッドで、decimalの型チェックが重複していたので削除したという内容になります。

PRの実装
public static bool IsKnownComparable<T>() =>
..
    typeof(T) == typeof(ulong) ||
    typeof(T) == typeof(decimal) || // decimalかどうか型チェック
    typeof(T) == typeof(float) ||
    typeof(T) == typeof(double) ||
    typeof(T) == typeof(decimal) ||  // あれ?もう一回チェックしている

そんなことあるかと最初見つけたときは思いましたが、以前にもソース自体は確認していたはずなので、ソース全体を通してみていると意外に気づかないものだなと思いました。
一応このPRを出してみたところ、10分もたたない内にapprovedされて自動マージ状態になって驚いたのは良い思い出ですw。

[#125893] Improve HashSet<T> performance by enabling JIT bounds check elimination

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

2026/03/22にマージされている最近の中でも最近のPRになります。
内容自体はかなり単純で、HashSet<T>の値検索時に行われるハッシュ探索処理のループ条件をwhile (i >= 0)while ((uint)i < (uint)entries.Length)に変更するだけというものです。
.NETのよく知られているuintチェックに変えることで、JITによる境界チェックを削除する最適化が有効になります。

変更点
- while (i >= 0)
+ while ((uint)i < (uint)entries.Length)
{

そして、HashSet<T>Tが値型の場合、EqualityComparer<T>.Default.Equalsが脱仮想化するため、実行コスト(命令数)がかなり少なくなります。(たしか1命令になったはず...)
これにより、ループ内の一要素あたりの実行コストはかなり小さくなっているので、相対的に境界値チェックのコストは意外に大きく占めていたようですが、今回の対応で境界値チェック分のコストが無くなったことにより、パフォーマンスとしてもある程度の効果が生まれたようです。

https://github.com/dotnet/runtime/blob/v11.0.100-preview.2.26159.112/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/HashSet.cs#L230-L252

まとめ

Windows7固有コードの削除や、日本語環境でのビルドエラーなど個人的にはちょっと特殊なPRを見つけたときにはいつもちょっと驚きます。
JITコンパイラ側の境界値チェック削除によるパフォーマンス改善対応については、ここで紹介していないPRでも色々と行われており、現在進行形でSpan<T>での境界値チェックを無くす対応はまだまだ行われている印象です。

ということで今回はこのへんで。

脚注
  1. PR Digest.NET - https://prozolic.github.io/PRDigest.NET/ ↩︎

  2. dotnet/coreclr coreclr - https://github.com/dotnet/coreclr/pull/13748 ↩︎

  3. dotnet/runtime Redundant bound checks for arr[^N-1] after arr[^N] - https://github.com/dotnet/runtime/issues/124531 ↩︎

  4. dotnet/runtime codereview SKILL.md - https://github.com/dotnet/runtime/blob/main/.github/skills/code-review/SKILL.md ↩︎

Discussion