公共领域还好吗?

时间:2009-09-11 12:45:37

标签: c# .net performance optimization field

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

我正在研究一个相当大规模的工程应用程序,该应用程序创建并使用结构的内存模型(从高层建筑到桥梁到棚子,无关紧要)。该项目涉及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秒,但为什么不呢?

12 个答案:

答案 0 :(得分:31)

您的测试对基于属性的版本并不公平。 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) 

答案 1 :(得分:21)

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

答案 2 :(得分:10)

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

在我被大量投票之前,我应该补充说封装是一件好事。给定不变“如果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;
    }
}

(更好的是提供初始化构造函数并使类不可变)。

答案 3 :(得分:3)

我知道你觉得这样做很脏,但是当性能成为一个问题时,规则和指导方针被射到地狱的情况并不少见。例如,使用MySQL的相当多的高流量网站都有数据复制和非规范化表。其他go even crazier

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

答案 4 :(得分:3)

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

Rico Mariani有几个相关的帖子:

答案 5 :(得分:1)

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

其他时候,这样做感觉太“错误”。

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

答案 6 :(得分:1)

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

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

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

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

答案 7 :(得分:1)

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

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

答案 8 :(得分:0)

以下是一些可以接受的方案(来自框架设计指南一书):

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

不是这样的地方:

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

根据你所说的,我不明白为什么你的琐碎财产不被JIT内联?

答案 9 :(得分:0)

如果您修改测试以使用您指定的临时变量而不是直接访问计算中的属性,您将看到性能大幅提升:

        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();

答案 10 :(得分:0)

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

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

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

在这里,您或您的客户要求表现。在你的项目中,如果我理解正确的话,绩效就是CRUCIAL。

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

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

祝你有个美好的一天!

答案 11 :(得分:0)

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