はじめに
「自動実装プロパティのget/setの速さってフィールドの読み書きと比べてどうなんだろう?」と思ったので簡単に調べました。
環境
Windows 8.1 (変なの使っててスミマセン)
Microsoft Visual Studio Community 2019 Version 16.4.3
.Net Framework 4.7.2
ILSpy version 5.0.2.5153
BenchmarkDotNet v0.12.0
プロパティの操作 ≒ メソッド呼び出し
プロパティに対してはフィールドのようにアクセスできますが、実際にはメソッドの呼び出しが行われます。フィールドの場合はメソッドの呼び出しは行われません。(なので私はフィールドへの読み書きの方が自動実装プロパティのget/setより少し速いだろうと思っていました)
プロパティに対する読み書きがメソッド呼び出しになっていることを確かめてみましょう。下のコードをビルドし、ILのコードを確認します。
usingSystem;namespaceConsoleApp11{publicclassPerson{/// <summary>/// プロパティ/// </summary>publicstringName{get;set;}/// <summary>/// フィールド (パブリックフィールドはホントは使っちゃダメだぞ)/// </summary>publicintAge;}classProgram{staticvoidMain(string[]args){varp=newPerson(){Name="taro",Age=8};Console.WriteLine(p.Name);Console.WriteLine(p.Age);}}}上のコードをビルドして作ったexeファイルをILSpyで開くとILのコードを見ることができます。Nameプロパティへの読み書きはget_Nameとset_Nameになっていることが分かります。Ageフィールドへの読み書きはstfldとldfldという命令になっています。
インライン化
プロパティへの読み書きはメソッド呼び出しとなり、フィールドへの読み書きはメモリの操作になることが分かりました。メソッド呼び出しを行う分、プロパティの方がフィールドよりも遅くなりそうな気がしますが、ILがネイティブコードに変換される過程がまだ残っています。
プログラムを実行するとき、ILはランタイムによってネイティブコードに変換されます(JITコンパイル)。そしてこのときに、メソッド呼び出しのコードが呼び出し先のメソッド中にある処理に置き換えられることがあります(インライン化)。
インライン化が行われる条件について正確なことは分からないのですが、自動実装プロパティのget/setのように、処理内容が単純な場合はインライン化されるようです。
コードを使って少し実験してみます。まずはインライン化を行わない場合です。Nameプロパティに[MethodImpl(MethodImplOptions.NoInlining)]属性を付けてインライン化を抑制します。
publicclassPerson{publicstringName{[MethodImpl(MethodImplOptions.NoInlining)]get;[MethodImpl(MethodImplOptions.NoInlining)]set;}publicintAge;}NameプロパティのsetのJITコンパイル後の内コードを見てみます。プロパティに値を代入する箇所にブレークポイントを置き、処理が止まったら逆アセンブリウィンドウを開きます。逆アセンブリウィンドウ上でもC#のコードのようにステップ実行できるのでブレークポイントの後ろにあるcall命令のところまで処理を進めます。
ステップインするとsetメソッドの中に入れますが、入った先にもう一つcall命令があります。2つ目のcall命令を実行するとプロパティの値が書き変わります。ただ、2つ目のcall命令のジャンプ先には逆アセンブリウィンドウからの操作では進めず、これ以上のことは分かりませんでした。(理由はよく分からない)
次にインライン化を有効にした場合を見てみます。Nameプロパティに付ける属性の内容を変えます。
publicclassPerson{publicstringName{[MethodImpl(MethodImplOptions.AggressiveInlining)]get;[MethodImpl(MethodImplOptions.AggressiveInlining)]set;}publicintAge;}先ほどと同じようにNameプロパティに値を設定するところで処理を止めて様子を見てみます。
call 7467EC30は先ほどのインライン化させない場合のときの、2つ目のcall命令の内容と一致しています。また、1つ目のcall命令は見当たらなくなりました。恐らくですがインライン化により削除されたのではないかと思います。
ベンチマーク
インライン化の雰囲気を感じられたところで、プロパティとフィールドに対する読み書きの時間を比べてみようと思います。ベンチマークにはBenchmarkDotNet を使いました。以下のコードをリリースビルドし、実行します。
usingSystem.Linq;usingSystem.Runtime.CompilerServices;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;namespaceConsoleApp10{publicclassPropertyVsField{publicintAggressiveInlining{[MethodImpl(MethodImplOptions.AggressiveInlining)]get;[MethodImpl(MethodImplOptions.AggressiveInlining)]set;}publicintNoInlining{[MethodImpl(MethodImplOptions.NoInlining)]get;[MethodImpl(MethodImplOptions.NoInlining)]set;}publicintDefault{get;set;}publicintField;}publicclassPropBenchmark{publicPropertyVsFieldpropVsField=newPropertyVsField(){AggressiveInlining=42,NoInlining=42,Default=42,Field=42};[Benchmark]publicintAggressiveInlining(){intv=0;foreach(var_inEnumerable.Range(1,10)){v+=this.propVsField.AggressiveInlining;}returnv;}[Benchmark]publicintNoInlining(){intv=0;foreach(var_inEnumerable.Range(1,10)){v+=this.propVsField.NoInlining;}returnv;}[Benchmark]publicintDefault(){intv=0;foreach(var_inEnumerable.Range(1,10)){v+=this.propVsField.AggressiveInlining;}returnv;}[Benchmark]publicintField(){intv=0;foreach(var_inEnumerable.Range(1,10)){v+=this.propVsField.AggressiveInlining;}returnv;}}classProgram{staticvoidMain(string[]args){varsummary=BenchmarkRunner.Run<PropBenchmark>();}}}※1回のgetの呼び出しだと処理時間が短すぎて差が確認できなかったのでforeachで回しています
※getとsetで結果に違いが出るとは考えにくかったのでgetだけ調べました。(値の設定はフィールドの方が速いけど、参照はプロパティの方が速いとかそんなことにはならないだろうと思った。setではなくgetにした理由は特にないです。)
実行結果は私の環境ではこんな感じになりました。
NoInlining以外は大体同じくらいの時間(Mean)になっているので、AggressiveInliningを指定しなくてもインライン化が行われてフィールドへの操作と同じくらいの効率でプロパティへのアクセスができていることが分かります。
参考
自動実装するプロパティ (C# プログラミング ガイド)
フィールド (C# プログラミング ガイド)
More Effective C# 6.0/7.0 項目1 アクセス可能なデータメンバーの代わりにプロパティを使おう
++C++; [フレームワーク / 実行環境] JITコンパイル
++C++; [構造化] [雑記] インライン化
Visual Studio デバッガーでの逆アセンブリ コードの表示
ILSpy
BenchmarkDotNet

