字段与属性.性能优化



请注意,此问题仅与性能有关。让我们跳过设计准则、哲学、兼容性、可移植性以及任何与纯性能无关的内容。非常感谢。

现在开始提问。我一直认为,因为C#getters/ssetters实际上是伪装的方法,所以读取公共字段的速度一定比调用getter更快。

所以为了确保我做了一个测试(下面的代码)。但是,如果您从Visual Studio内部运行该测试,则该测试只会产生预期结果(即字段比getter快34%)

一旦你从命令行运行它,它显示的时间几乎相同。。。

唯一的解释可能是CLR进行了额外的优化(如果我错了,请纠正我)。

我不相信在实际应用中,如果这些特性以更复杂的方式使用,它们会以同样的方式进行优化。

请帮我证明或反驳这样一种观点,即在现实生活中,属性比字段慢。

问题是-我应该如何修改测试类以使CLR改变行为,从而使公共字段的性能超过getter。或者告诉我,任何没有内部逻辑的属性都将执行与字段相同的操作(至少在getter上)

编辑:我只是在谈论x64版本

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace PropertyVsField
{
    class Program
    {
        static int LEN = 20000000;
        static void Main(string[] args)
        {
            List<A> a = new List<A>(LEN);
            List<B> b = new List<B>(LEN);
            Random r = new Random(DateTime.Now.Millisecond);
            for (int i = 0; i < LEN; i++)
            {
                double p = r.NextDouble();
                a.Add(new A() { P = p });
                b.Add(new B() { P = p });
            }
            Stopwatch sw = new Stopwatch();
            double d = 0.0;
            sw.Restart();
            for (int i = 0; i < LEN; i++)
            {
                d += a[i].P;
            }
            sw.Stop();
            Console.WriteLine("auto getter. {0}. {1}.", sw.ElapsedTicks, d);
            sw.Restart();
            for (int i = 0; i < LEN; i++)
            {
                d += b[i].P;
            }
            sw.Stop();
            Console.WriteLine("      field. {0}. {1}.", sw.ElapsedTicks, d);
            Console.ReadLine();
        }
    }
    class A
    {
        public double P { get; set; }
    }
    class B
    {
        public double P;
    }
}

正如其他人已经提到的,getter是内联的。

如果你想避免内联,你必须使用

  • 将自动属性替换为手动属性:

    class A 
    {
        private double p;
        public double P
        {
            get { return p; }
            set { p = value; }
        }
    } 
    
  • 并告诉编译器不要内联getter(如果你喜欢的话,也可以同时内联两者):

            [MethodImpl(MethodImplOptions.NoInlining)]
            get { return p; }
    

请注意,第一个更改不会对性能产生影响,而第二个更改显示了明显的方法调用开销:

手动属性:

auto getter. 519005. 10000971,0237547.
      field. 514235. 20001942,0475098.

getter没有内联:

auto getter. 785997. 10000476,0385552.
      field. 531552. 20000952,077111.

看看属性与字段——为什么重要?(Jonathan Aneja)MSDN上VB团队成员的博客文章。他概述了性质与字段的争论,并解释了琐碎的性质如下:

我听说过一个关于使用字段而不是属性的论点是"字段更快",但对于琐碎的属性true,因为CLR的实时(JIT)编译器将内联属性访问并生成与访问字段。

JIT将内联其内部度量确定的内联速度更快的任何方法(而不仅仅是getter)。假设一个标准属性是return _Property;,那么它在任何情况下都将被内联。

您看到不同行为的原因是,在附加了调试器的调试模式下,JIT会受到严重的阻碍,以确保任何堆栈位置都与您期望的代码相匹配。

你也忘记了表现的第一条规则,测试胜过思考。例如,尽管快速排序在渐近上比插入排序快,但对于极小的输入,插入排序实际上更快。

唯一的解释可能是CLR进行了额外的优化(如果我错了,请更正我)。

是的,它被称为内联。它是在编译器中完成的(机器代码级别,即JIT)。由于getter/setter是琐碎的(即非常简单的代码),因此方法调用被销毁,getter/sette被写在周围的代码中。

这在调试模式下不会为了支持调试而发生(即在getter或setter中设置断点的能力)。

在visualstudio中,没有办法在调试器中做到这一点。编译发行版,在没有附加调试器的情况下运行,您将获得完整的优化。

我不相信在实际应用中,如果这些特性以更复杂的方式使用,它们会以同样的方式进行优化。

这个世界充满了错误的幻想。它们将被优化,因为它们仍然是琐碎的(即简单的代码,所以它们是内联的)。

在阅读了您的所有文章后,我决定使用以下代码进行基准测试:

    [TestMethod]
    public void TestFieldVsProperty()
    {
        const int COUNT = 0x7fffffff;
        A a1 = new A();
        A a2 = new A();
        B b1 = new B();
        B b2 = new B();
        C c1 = new C();
        C c2 = new C();
        D d1 = new D();
        D d2 = new D();
        Stopwatch sw = new Stopwatch();
        long t1, t2, t3, t4;
        sw.Restart();
        for (int i = COUNT - 1; i >= 0; i--)
        {
            a1.P = a2.P;
        }
        sw.Stop();
        t1 = sw.ElapsedTicks;
        sw.Restart();
        for (int i = COUNT - 1; i >= 0; i--)
        {
            b1.P = b2.P;
        }
        sw.Stop();

        t2 = sw.ElapsedTicks;
        sw.Restart();
        for (int i = COUNT - 1; i >= 0; i--)
        {
            c1.P = c2.P;
        }
        sw.Stop();
        t3 = sw.ElapsedTicks;
        sw.Restart();
        for (int i = COUNT - 1; i >= 0; i--)
        {
            d1.P = d2.P;
        }
        sw.Stop();

        t4 = sw.ElapsedTicks;
        long max = Math.Max(Math.Max(t1, t2), Math.Max(t3, t4));
        Console.WriteLine($"auto: {t1}, {max * 100d / t1:00.00}%.");
        Console.WriteLine($"field: {t2}, {max * 100d / t2:00.00}%.");
        Console.WriteLine($"manual: {t3}, {max * 100d / t3:00.00}%.");
        Console.WriteLine($"no inlining: {t4}, {max * 100d / t4:00.00}%.");
    }
    class A
    {
        public double P { get; set; }
    }
    class B
    {
        public double P;
    }
    class C
    {
        private double p;
        public double P
        {
            get => p;
            set => p = value;
        }
    }
    class D
    {
        public double P
        {
            [MethodImpl(MethodImplOptions.NoInlining)]
            get;
            [MethodImpl(MethodImplOptions.NoInlining)]
            set;
        }
    }

当在调试模式下测试时,我得到了这样的结果:

auto: 35142496, 100.78%.
field: 10451823, 338.87%.
manual: 35183121, 100.67%.
no inlining: 35417844, 100.00%.

但当切换到释放模式时,结果与以前不同。

auto: 2161291, 873.91%.
field: 2886444, 654.36%.
manual: 2252287, 838.60%.
no inlining: 18887768, 100.00%.

汽车房地产似乎是一种更好的方式。

需要注意的是,在Visual Studio中可以看到"真实"的性能。

  1. 在启用优化的发布模式下编译
  2. 转到"调试"->"选项和设置",取消选中"在模块加载时抑制JIT优化(仅限托管)"
  3. (可选)取消选中"仅启用我的代码",否则您可能无法介入代码

现在,即使附加了调试器,jitted程序集也将是相同的,如果愿意的话,您可以介入优化的disassembly。这对于理解CLR如何优化代码至关重要。

最新更新