.NET Frameworkと.NET CoreでDictionaryの性能が異なる。
ランダムな整数をキーとして、AddとTryGetValueを5000000回行った結果10回の平均時間(ms)は以下の通りである。
バージョン | Add | TryGetValue |
---|---|---|
.NET Framework 4.6 | 681.8 | 268.5 |
.NET Core 2.1 | 585.9 | 266.6 |
TryGetValueはほぼ同じだが、Addは.NET Core 2.1の方が1.16倍速い。
.NET Frameworkと.NET Coreで、Dictionaryの実装にどのような差があるか調べたところ、一つはハッシュエントリの使用方法が改良されていたのと、もう一つはref localを使う変更がされていた。
.NET Framework 4.8のソース
.NET Core 2.1のソース
以下は、ref localについて調べた内容である。
ref local
ref localは、例えば以下のようなコードで、
entries[index].hashCode = hashCode; entries[index].next = buckets[targetBucket]; entries[index].key = key; entries[index].value = value;
配列の要素に同じインデックスでアクセスしているところを
ref Entry entry = ref entries[index]; entry.hashCode = hashCode; entry.next = buckets[targetBucket]; entry.key = key; entry.value = value;
のように、C++の参照と同じように、参照を示す変数に格納してからアクセスする方法だ。
参考:Ref local reassignment - C# 7.3 specification proposals | Microsoft Docs
なお、配列の要素にref localを使用するには、C# 7.2以上でないないとコンパイルエラーとなるため、.csprojに以下の設定を加える必要がある。(この例では、.NET Core 2.1はC# 7.3に対応しているので、7.3としている。)
<PropertyGroup> <LangVersion>7.3</LangVersion> </PropertyGroup>
ref localの効果測定
次に、この部分だけを変更してみて性能にどれだけ差があるか調べてみた。
変更元のソースは、.NET Framework 4.8のソースとした。
https://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs
上記の部分の変更前後で測定結果は以下の通りとなった。(変更箇所はAddのみ)
Add | TryGetValue | |
---|---|---|
変更前 | 617.6 | 262.5 |
変更後 | 608.6 | 261.8 |
わずか(1.01倍)だが、ref localを使用すると速くなる。
ILDasmで生成されたILを比較してみた。
上記の変更箇所は以下の通りとなっていた。
変更前
IL_0133: ldloc.0 IL_0134: stfld System.Int32 MyDictionary1.Dictionary`2/Entry<TKey,TValue>::hashCode IL_0139: ldarg.0 IL_013a: ldfld MyDictionary1.Dictionary`2/Entry<TKey,TValue>[] MyDictionary1.Dictionary`2<TKey,TValue>::entries IL_013f: ldloc.2 IL_0140: ldelema MyDictionary1.Dictionary`2/Entry<TKey,TValue> IL_0145: ldarg.0 IL_0146: ldfld System.Int32[] MyDictionary1.Dictionary`2<TKey,TValue>::buckets IL_014b: ldloc.1 IL_014c: ldelem.i4 IL_014d: stfld System.Int32 MyDictionary1.Dictionary`2/Entry<TKey,TValue>::next IL_0152: ldarg.0 IL_0153: ldfld MyDictionary1.Dictionary`2/Entry<TKey,TValue>[] MyDictionary1.Dictionary`2<TKey,TValue>::entries IL_0158: ldloc.2 IL_0159: ldelema MyDictionary1.Dictionary`2/Entry<TKey,TValue> IL_015e: ldarg.1 IL_015f: stfld TKey MyDictionary1.Dictionary`2/Entry<TKey,TValue>::key IL_0164: ldarg.0 IL_0165: ldfld MyDictionary1.Dictionary`2/Entry<TKey,TValue>[] MyDictionary1.Dictionary`2<TKey,TValue>::entries IL_016a: ldloc.2 IL_016b: ldelema MyDictionary1.Dictionary`2/Entry<TKey,TValue> IL_0170: ldarg.2 IL_0171: stfld TValue MyDictionary1.Dictionary`2/Entry<TKey,TValue>::value IL_0176: ldarg.0 IL_0177: ldfld System.Int32[] MyDictionary1.Dictionary`2<TKey,TValue>::buckets
変更後
IL_0133: dup IL_0134: ldloc.0 IL_0135: stfld System.Int32 MyDictionary1.Dictionary`2/Entry<TKey,TValue>::hashCode IL_013a: dup IL_013b: ldarg.0 IL_013c: ldfld System.Int32[] MyDictionary1.Dictionary`2<TKey,TValue>::buckets IL_0141: ldloc.1 IL_0142: ldelem.i4 IL_0143: stfld System.Int32 MyDictionary1.Dictionary`2/Entry<TKey,TValue>::next IL_0148: dup IL_0149: ldarg.1 IL_014a: stfld TKey MyDictionary1.Dictionary`2/Entry<TKey,TValue>::key IL_014f: ldarg.2 IL_0150: stfld TValue MyDictionary1.Dictionary`2/Entry<TKey,TValue>::value
変更前は25行、変更後は14行と、ILが短くなっているのが確認できる。
ILDasmについて
なお、ILDasmは.NET FrameworkのSDKに付属するが、.NET Coreの場合は、
dotnet tool install --global dotnet-ildasm
でインストールできる。
使う際は、
dotnet-ildasm CSDictionaryTest.dll >CSDictionaryTest.il
のようにして使う。結果は標準出力に出力されるのでリダイレクトするとよい。
まとめ
配列を同じインデックスで複数回アクセスしている場合、ref localを使うとわずかだが速くできる。
測定に使用したソースをGitHubに置いておいた。
GitHub - TadaoYamaoka/CSDictionaryTest
MyDictionary1.csが.NET Framework 4.8のDictionaryを元にしたソース。
MyDictionary2.csが.NET Core 2.1を元にしたソース。
MyDictionary3.csは他の実験したソースなのでこの記事とは無関係。