パブリックフィールドは大丈夫ですか?
-
05-07-2019 - |
質問
最初にやったように、あなたが腸から反応する前に、質問全体を読んでください。私は彼らがあなたを汚い気分にさせることを知っています、私たちはすべて以前に燃やされたことを知っています、そしてそれは「良いスタイル」ではありませんしかし、パブリックフィールドは大丈夫ですか?
私は、構造のインメモリモデル(高層ビルから橋を架けるものまで、何でも構いません)を作成および操作するかなり大規模なエンジニアリングアプリケーションに取り組んでいます。このプロジェクトには、幾何学分析と計算のトンがあります。これをサポートするために、モデルはポイント、ラインセグメントなどを表す多くの小さな不変の読み取り専用構造体で構成されています。これらの構造体の値の一部(ポイントの座標など)は、典型的なプログラム実行中の時間。モデルは複雑で計算量が多いため、パフォーマンスは非常に重要です。
私たちは、アルゴリズムの最適化、ボトルネックの特定、適切なデータ構造の使用などのために、できる限りのことを行っていると感じています。これは時期尚早な最適化のケースではないと思います。パフォーマンステストは、オブジェクトのプロパティを介してではなく、フィールドに直接アクセスするときに、(少なくとも)大きさの順序を示すパフォーマンスを向上させます。この情報と、データバインディングやその他の状況をサポートするプロパティと同じ情報を公開できるという事実を考えると、これは大丈夫ですか? 不変の構造体のフィールドのみを読み取ることを忘れないでください。これを後悔する理由は誰にも思い付くことができますか?
サンプルテストアプリは次のとおりです。
struct Point {
public Point(double x, double y, double z) {
_x = x;
_y = y;
_z = z;
}
public readonly double _x;
public readonly double _y;
public readonly double _z;
public double X { get { return _x; } }
public double Y { get { return _y; } }
public double Z { get { return _z; } }
}
class Program {
static void Main(string[] args) {
const int loopCount = 10000000;
var point = new Point(12.0, 123.5, 0.123);
var sw = new Stopwatch();
double x, y, z;
double calculatedValue;
sw.Start();
for (int i = 0; i < loopCount; i++) {
x = point._x;
y = point._y;
z = point._z;
calculatedValue = point._x * point._y / point._z;
}
sw.Stop();
double fieldTime = sw.ElapsedMilliseconds;
Console.WriteLine("Direct field access: " + fieldTime);
sw.Reset();
sw.Start();
for (int i = 0; i < loopCount; i++) {
x = point.X;
y = point.Y;
z = point.Z;
calculatedValue = point.X * point.Y / point.Z;
}
sw.Stop();
double propertyTime = sw.ElapsedMilliseconds;
Console.WriteLine("Property access: " + propertyTime);
double totalDiff = propertyTime - fieldTime;
Console.WriteLine("Total difference: " + totalDiff);
double averageDiff = totalDiff / loopCount;
Console.WriteLine("Average difference: " + averageDiff);
Console.ReadLine();
}
}
結果:
直接フィールドアクセス:3262
プロパティアクセス:24248
合計差:20986
平均差:0.00020986
わずか 21秒ですが、なぜですか?
解決
テストは、プロパティベースのバージョンに対して実際には公平ではありません。 JITは単純なプロパティをインライン化するのに十分スマートなので、フィールドへの直接アクセスと同等の実行時パフォーマンスが得られますが、プロパティが定数値にアクセスするタイミングを検出するほどスマートではありません(今日)。
この例では、フィールドアクセスバージョンのループ本体全体が最適化されて、次のようになります。
for (int i = 0; i < loopCount; i++)
00000025 xor eax,eax
00000027 inc eax
00000028 cmp eax,989680h
0000002d jl 00000027
}
2番目のバージョンでは、各反復で実際に浮動小数点除算を実行しています:
for (int i = 0; i < loopCount; i++)
00000094 xor eax,eax
00000096 fld dword ptr ds:[01300210h]
0000009c fdiv qword ptr ds:[01300218h]
000000a2 fstp st(0)
000000a4 inc eax
000000a5 cmp eax,989680h
000000aa jl 00000096
}
アプリケーションをわずかに2つ変更するだけで、アプリケーションをより現実的にすることで、2つの操作のパフォーマンスが実質的に同じになります。
最初に、入力値が定数ではなく、JITが除算を完全に削除するほど賢くないように、入力値をランダム化します。
変更元:
Point point = new Point(12.0, 123.5, 0.123);
to:
Random r = new Random();
Point point = new Point(r.NextDouble(), r.NextDouble(), r.NextDouble());
次に、各ループ反復の結果がどこかで使用されていることを確認します:
各ループの前に、calculatedValue = 0を設定して、両方が同じポイントで開始するようにします。各ループの後、Console.WriteLine(calculatedValue.ToString())を呼び出して、結果が「使用済み」であることを確認します。そのため、コンパイラは最適化を行いません。最後に、ループの本文を&quot; calculatedValue = ...&quot;から変更します。 &quot; calculatedValue + = ...&quot;各反復が使用されるようにします。
私のマシンでは、これらの変更(リリースビルドを使用)により次の結果が得られます。
Direct field access: 133
Property access: 133
Total difference: 0
Average difference: 0
予想どおり、これらの変更された各ループのx86は同一です(ループアドレスを除く)
000000dd xor eax,eax
000000df fld qword ptr [esp+20h]
000000e3 fmul qword ptr [esp+28h]
000000e7 fdiv qword ptr [esp+30h]
000000eb fstp st(0)
000000ed inc eax
000000ee cmp eax,989680h
000000f3 jl 000000DF (This loop address is the only difference)
他のヒント
読み取り専用フィールドを持つ不変オブジェクトを扱っていることを考えると、パブリックフィールドが汚い習慣であることがわからない場合は、1つのケースにヒットしたと言えます。
IMO、「パブリックフィールドなし」ルールは技術的に正しいルールの1つですが、一般の人々が使用することを目的としたライブラリを設計していない限り、それを破っても問題が発生することはほとんどありません。
私があまりにも多くの票を投じられる前に、カプセル化は良いことだと付け加えるべきです。不変式「HasValueがfalseの場合、Valueプロパティはnullでなければならない」を考えると、この設計には欠陥があります:
class A {
public bool HasValue;
public object Value;
}
ただし、その不変条件を考えると、この設計にも同様に欠陥があります:
class A {
public bool HasValue { get; set; }
public object Value { get; set; }
}
正しいデザインは
class A {
public bool HasValue { get; private set; }
public object Value { get; private set; }
public void SetValue(bool hasValue, object value) {
if (!hasValue && value != null)
throw new ArgumentException();
this.HasValue = hasValue;
this.Value = value;
}
}
(さらに良いのは、初期化コンストラクターを提供し、クラスを不変にすることです。)
これをやるのはちょっと汚い気がするのは知っていますが、パフォーマンスが問題になったときにルールやガイドラインが大打撃を受けるのは珍しいことではありません。たとえば、MySQLを使用するトラフィックの多いWebサイトには、データの重複と非正規化されたテーブルがあります。その他さらに気が狂う。
物語の教訓-それはあなたが教えられたり助言されたりしたすべてに反するかもしれませんが、ベンチマークは嘘をつきません。それがうまくいくなら、それをしてください。
追加のパフォーマンスが本当に必要な場合は、おそらくおそらく正しいことです。追加のパフォーマンスが必要ない場合は、おそらく必要ありません。
Rico Marianiには、関連する投稿がいくつかあります:
個人的に、パブリックフィールドの使用を検討するのは、実装固有の非常にプライベートなネストされたクラスのみです。
それ以外の場合は、「間違っている」と感じることがあります。それを行うには。
CLRは、問題とならないようにメソッド/プロパティを(リリースビルドで)最適化することでパフォーマンスを処理します。
他の答えやあなたの結論に反対するわけではありません...しかし、パフォーマンスの差の統計情報がどこから得られるのか知りたいです。 C#コンパイラを理解しているので、 simple プロパティ(フィールドへの直接アクセス以外の追加コードなし)は、いずれにしても直接アクセスとしてJITコンパイラによってインライン化されます。
これらの単純な場合(ほとんどの場合)でもプロパティを使用する利点は、プロパティとして記述することにより、プロパティを変更する可能性のある将来の変更を許可することでした。 (あなたの場合はもちろん、将来そのような変更はありませんが)
リリースビルドをコンパイルし、デバッガーではなく、exeから直接実行してみてください。アプリケーションがデバッガーを介して実行された場合、JITコンパイラーはプロパティアクセサーをインライン化しません。結果を再現できませんでした。実際、私が実行した各テストは、実行時間に実質的に差がないことを示しました。
しかし、他の人のように、私は直接フィールドアクセスに完全に反対しているわけではありません。特に、アプリケーションをコンパイルするためにコードを変更する必要なしに、後でフィールドをプライベートにし、パブリックプロパティアクセサーを追加するのは簡単だからです。
編集:最初のテストでは、doubleではなくintデータ型を使用しました。 doubleを使用すると、大きな違いが見られます。 intでは、直接とプロパティはほぼ同じです。 doublesプロパティへのアクセスは、マシンへの直接アクセスよりも約7倍遅いです。これはやや不可解です。
また、デバッガーの外部でテストを実行することも重要です。リリースビルドでも、デバッガーはオーバーヘッドを追加し、結果に歪みが生じます。
これは問題ないシナリオです(Framework Design Guidelinesブックから):
- 定数に定数フィールドを使用する それは決して変わりません。
- パブリックを使用する 事前定義の静的読み取り専用フィールド オブジェクトインスタンス。
それ以外の場合:
- 可変のインスタンスを割り当てないでください 読み取り専用フィールドに入力します。
あなたが述べたことから、あなたの些細な特性がJITによってインライン化されない理由がわかりませんか?
計算のプロパティに直接アクセスするのではなく、割り当てた一時変数を使用するようにテストを変更すると、パフォーマンスが大幅に向上します。
sw.Start();
for (int i = 0; i < loopCount; i++)
{
x = point._x;
y = point._y;
z = point._z;
calculatedValue = x * y / z;
}
sw.Stop();
double fieldTime = sw.ElapsedMilliseconds;
Console.WriteLine("Direct field access: " + fieldTime);
sw.Reset();
sw.Start();
for (int i = 0; i < loopCount; i++)
{
x = point.X;
y = point.Y;
z = point.Z;
calculatedValue = x * y / z;
}
sw.Stop();
おそらく私は他の誰かを繰り返しますが、もしそれが助けになるなら、ここにも私のポイントがあります。
教育は、そのような状況に遭遇したときにある程度の使いやすさを達成するために必要なツールを提供することです。
アジャイルソフトウェア開発方法論では、コードがどのように見えても、最初にクライアントに製品を提供する必要があるとされています。第二に、コードを最適化して、「美しい」コードにすることができます。またはプログラミングの最新技術に従って。
ここでは、あなたまたはあなたのクライアントがパフォーマンスを必要とします。あなたのプロジェクト内で、私が正しく理解していれば、パフォーマンスは重要です。
だから、コードがどのように見えるか、または「アート」を尊重するかどうかは気にしないということで、あなたは私に同意するでしょう。パフォーマンスとパワフルにするために必要なことをしてください!プロパティを使用すると、コードを「フォーマット」できます。必要に応じてデータI / O。プロパティには独自のメモリアドレスがあり、メンバーの値を返すときにメンバーアドレスを検索するため、アドレスを2回検索します。パフォーマンスが非常に重要な場合は、それを実行し、不変のメンバーを公開します。 :-)
正しく読めば、これは他のいくつかの観点も反映しています。 :)
良い一日を!
機能をカプセル化するタイプはプロパティを使用する必要があります。データを保持するだけの型は、不変クラスの場合を除き、パブリックフィールドを使用する必要があります(読み取り専用プロパティでフィールドをラップすることは、変更から確実に保護する唯一の方法です)。メンバーをパブリックフィールドとして公開すると、「これらのメンバーは、他のことを考慮せずにいつでも自由に変更できる」と宣言されます。問題の型がクラス型である場合、「このことへの参照を公開する人は、受信者がいつでも適切と思われる方法でこれらのメンバーを変更できるようにする」と宣言します。そのような宣言が不適切である場合、パブリックフィールドを公開するべきではありませんが、そのような宣言が適切であり、それによって有効化された仮定からクライアントコードが恩恵を受ける場合、パブリックフィールドを公開する必要があります。