公共领域还可以吗?

在你对肠道的反应之前,正如我最初所做的那样,请阅读整个问题。 我知道他们让你觉得很脏,我知道我们以前都被烧过,我知道这不是“好风格”,但公共场地还好吗?

我正在开发一个相当大规模的工程应用程序,它可以创建并使用结构的内存模型(从高层建筑到桥梁到棚屋,无关紧要)。 该项目涉及TON的几何分析和计算。 为了支持这一点,该模型由许多微小的不可变只读结构组成,用于表示点,线段等事物。这些结构的某些值(如点的坐标)可被访问数十亿或数亿典型程序执行期间的时间。 由于模型的复杂性和计算量,性能绝对至关重要。

我觉得我们正在尽我们所能来优化我们的算法,性能测试以确定瓶颈,使用正确的数据结构等。我不认为这是过早优化的情况。 性能测试显示直接访问字段而不是通过对象上的属性时性能提升的数量级 (至少)。 鉴于此信息,以及我们还可以公开与属性相同的信息以支持数据绑定和其他情况……这样可以吗? 请记住,只读不可变结构上的字段。 任何人都可以想到我会后悔的原因吗?

这是一个示例测试应用程序:


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 } 

而第二个版本,实际上是在每次迭代时执行浮点除法:

 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 } 

对应用程序进行两次小的更改以使其更加真实,这使得两个操作的性能几乎完全相同。

首先,随机化输入值,使它们不是常量,JIT不够智能,不能完全删除除法。

改变自:

 Point point = new Point(12.0, 123.5, 0.123); 

至:

 Random r = new Random(); Point point = new Point(r.NextDouble(), r.NextDouble(), r.NextDouble()); 

其次,确保在某处使用每个循环迭代的结果:

在每个循环之前,设置calculatedValue = 0,使它们都从同一点开始。 在每个循环之后调用Console.WriteLine(calculatedValue.ToString())以确保结果“已使用”,因此编译器不会对其进行优化。 最后,将循环体从“calculatedValue = ...”更改为“calculatedValue + = ...”,以便使用每次迭代。

在我的机器上,这些更改(使用发布版本)会产生以下结果:

 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) 

鉴于您使用只读字段处理不可变对象,我会说当我没有发现公共字段是一个肮脏的习惯时,你遇到了一个案例。

IMO,“无公共字段”规则是技术上正确的规则之一,但除非您正在设计一个供公众使用的库,否则如果您破坏它,则不太可能导致任何问题。

在我太过低调投票之前,我应该补充一点, 封装是一件好事。 给定不变“如果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的相当多的高流量网站都有数据复制和非规范化表。 其他人甚至更疯狂 。

故事的道德 – 它可能违背你所教授或建议的一切,但基准并不是谎言。 如果效果更好,就去做吧。

如果你真的需要额外的性能,那么它可能是正确的。 如果你不需要额外的性能那么它可能不是。

Rico Mariani有几个相关的post:

  • 关于基于价值的规划的十个问题
  • 关于基于价值的规划的十个问题:解决方案

就个人而言,我唯一一次考虑使用公共字段是在一个特定于实现的私有嵌套类中。

其他时候它只是感觉太“错误”。

CLR将通过优化方法/属性(在发布版本中)来处理性能,因此这不应该是一个问题。

并不是说我不同意其他答案,或者你的结论……但我想知道你从哪里得到数量级的性能差异。 据我了解C#编译器,任何简单的属性(除了直接访问该字段之外没有其他代码)都应该由JIT编译器作为直接访问内联。

即使在这些简单的情况下(在大多数情况下)使用属性的优点是,通过将其写为属性,您允许将来可能修改属性的更改。 (虽然在你的情况下,将来不会有任何这样的改变)

尝试编译发布版本并直接从exe而不是通过调试器运行。 如果应用程序是通过调试器运行的,那么JIT编译器将不会内联属性访问器。 我无法复制你的结果。 实际上,我运行的每个测试都表明执行时间几乎没有差异。

但是,和其他人一样,我并没有完全反对直接进行现场访问。 特别是因为很容易使字段私有并在以后添加公共属性访问器而不需要进行任何更多的代码修改以使应用程序编译。

编辑:好的,我的初始测试使用的是int数据类型而不是double。 我在使用双打时看到了巨大的差异。 通过整数,直接与财产几乎相同。 使用双打属性访问比我的机器上的直接访问慢约7倍。 这对我来说有点令人费解。

此外,在调试器外部运行测试也很重要。 即使在发布版本中,调试器也会增加开销,从而导致结果出现偏差。

这里有一些可行的方案(来自框架设计指南书):

  • 对永远不会改变的常量使用常量字段。
  • 请对预定义的对象实例使用公共静态只读字段。

而它不是:

  • 不要将可变类型的实例分配给只读字段。

从你所说的我不明白为什么你的琐碎财产没有被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(); 

也许我会重复别人,但如果这可能会有所帮助,这也是我的观点。

教学是为了给你提供在遇到这种情况时达到一定程度的轻松所需的工具。

敏捷软件开发方法说,无论您的代码是什么样,您都必须先将产品交付给客户。 其次,您可以优化并使您的代码“美观”或根据现有的编程状态。

在这里,您或您的客户需要性能。 如果我理解正确的话,在你的项目中,PERFORMANCE是CRUCIAL。

所以,我想你会同意我的意见,我们不关心代码的外观或是否尊重“艺术”。 做你需要的,使它高效和强大! 属性允许您的代码在需要时“格式化”数据I / O. 属性有自己的内存地址,然后在返回成员的值时查找其成员地址,因此您有两次地址搜索。 如果表现如此重要,那就去做吧,让你的不可变成员公开。 🙂

如果我正确阅读,这也反映了其他一些观点。 🙂

祝你有美好的一天!

封装function的类型应使用属性。 仅用于保存数据的类型应使用公共字段,但不可变类的情况除外(其中只读属性中的包装字段是可靠地保护它们不被修改的唯一方法)。 将成员公开为公共领域基本上宣称“这些成员可以随时自由修改而不考虑其他任何事情”。 如果所讨论的类型是类类型,它进一步宣称“任何暴露对此事物的引用的人都将允许接收者以他们认为合适的任何方式随时更改这些成员。” 虽然如果这样的宣言不合适,人们不应暴露公共领域,但如果这样的宣言适当且客户代码可以从这样的假设中受益,则应该公开公共领域。