【.NET】直近でマージされたPRの紹介 その2
はじめに
PR Digest.NET[1]で毎日、dotnet/runtimeでのマージされたPRをふわっと観測しているとたまに「なるほど!」と思うPRと出会うことがあります。
ということで、今回は比較的直近にマージされたPRの中で、個人的に印象がのこったPRや「そんなバグあったのか?」というPRを独断と偏見でまとめました。
※直近かどうかはあやしいですが、上げているPRは2026年にマージされたものなので、直近とさせてください。
前回は以下になります。
確認環境
一部動作の再現確認には、記事を投稿した時点で正式リリース前の最新プレビュー版(.NET 11 preview2)またはdotnet/runtimeのローカルビルドランタイムでデバッグして確認しています。
[#122779] Intrinsify Enum.Equals to avoid boxing
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)で実装されているため、仕方ないという気もしますが。
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命令に変更されて、実にシンプルな出力になりました!
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の特殊対応の仲間入りをしました。
JIT側では、Enum.HasFlag[2]で行われた最適化対応とほぼ同じような対応が実装されました。
実装部分としては、src/coreclr/gentree.cppのCompiler::gtFoldExprCallにてNI_System_Enum_Equals分岐処理が追加されています。
ここでは、引数のEnumがfloatとdouble以外の数値型であるかなどのチェックを行い、今回の最適化対象である場合には、数値として直接比較する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
この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]
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
System.Net.Securityのメソッドで参照しているWindows7固有のソースを削除しているPRになります。
Windows7は2020年1月14日にサポートが終了していますが、意外にもまだWindows7の固有のコードが残っていたことに驚いたので追加しました。
[#124737] Remove duplicate typeof(decimal) check in IsKnownComparable
私が最近出したPRになりますが、中身は一番大したことない修正になります。
System.Collections.ImmutableプロジェクトのConstants.IsKnownComparable<T>メソッドで、decimalの型チェックが重複していたので削除したという内容になります。
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
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命令になったはず...)
これにより、ループ内の一要素あたりの実行コストはかなり小さくなっているので、相対的に境界値チェックのコストは意外に大きく占めていたようですが、今回の対応で境界値チェック分のコストが無くなったことにより、パフォーマンスとしてもある程度の効果が生まれたようです。
まとめ
Windows7固有コードの削除や、日本語環境でのビルドエラーなど個人的にはちょっと特殊なPRを見つけたときにはいつもちょっと驚きます。
JITコンパイラ側の境界値チェック削除によるパフォーマンス改善対応については、ここで紹介していないPRでも色々と行われており、現在進行形でSpan<T>での境界値チェックを無くす対応はまだまだ行われている印象です。
ということで今回はこのへんで。
-
PR Digest.NET - https://prozolic.github.io/PRDigest.NET/ ↩︎
-
dotnet/coreclr coreclr - https://github.com/dotnet/coreclr/pull/13748 ↩︎
-
dotnet/runtime Redundant bound checks for arr[^N-1] after arr[^N] - https://github.com/dotnet/runtime/issues/124531 ↩︎
-
dotnet/runtime codereview SKILL.md - https://github.com/dotnet/runtime/blob/main/.github/skills/code-review/SKILL.md ↩︎
Discussion