公共领域还好吗?

MKi*_*ing 22 .net c# optimization performance 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

结果:
直接现场访问:3262
物业访问:24248
总差异:20986
平均差异:0.00020986


只有 21秒,但为什么不呢?

Sta*_*ker 31

您的测试对于基于属性的版本并不公平.JIT足够聪明,可以内联简单属性,使它们具有与直接字段访问相当的运行时性能,但是(当今)检测属性何时访问常量值似乎不够智能.

在您的示例中,字段访问版本的整个循环体被优化掉,变为:

for (int i = 0; i < loopCount; i++)
00000025  xor         eax,eax 
00000027  inc         eax  
00000028  cmp         eax,989680h 
0000002d  jl          00000027 
}
Run Code Online (Sandbox Code Playgroud)

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

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 
}
Run Code Online (Sandbox Code Playgroud)

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

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

改变自:

Point point = new Point(12.0, 123.5, 0.123);
Run Code Online (Sandbox Code Playgroud)

至:

Random r = new Random();
Point point = new Point(r.NextDouble(), r.NextDouble(), r.NextDouble());
Run Code Online (Sandbox Code Playgroud)

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

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

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

Direct field access: 133
Property access: 133
Total difference: 0
Average difference: 0
Run Code Online (Sandbox Code Playgroud)

正如我们所期望的那样,每个修改过的循环的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) 
Run Code Online (Sandbox Code Playgroud)

  • 只需在调试器外部运行优化的发布版本(如果启动附加了调试器的托管进程,它将更改JIT行为).然后,当它在最后的Console.ReadLine中等待时,将VS调试器附加到进程(Tools-> Attach To Process).然后在调用堆栈窗口中右键单击堆栈框架并选择"Go To Disassembly" (8认同)
  • 很棒的答案.这在衡量绩效时表现出一个*关键*因素:始终衡量真实世界的表现.除非你是建立基准测试的专家,否则你将编写一个测量一些完全没有意义的东西的基准测试,就像这样做. (8认同)
  • 另外,你是如何得到JIT编译的x86代码的? (2认同)

Fre*_*örk 21

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

  • 同意,但理想情况下,只有在有明显的性能要求时. (11认同)

eri*_*len 10

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

在我被大量投票之前,我应该补充一点,封装是一件好事.给定不变"如果HasValue为false,则Value属性必须为null",这种设计存在缺陷:

class A {
    public bool HasValue;
    public object Value;
}
Run Code Online (Sandbox Code Playgroud)

但是,鉴于这种不变性,这种设计同样存在缺陷:

class A {
    public bool HasValue { get; set; }
    public object Value { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

正确的设计是

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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