领域与财产。性能优化


75

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

现在到问题。我一直以为,因为C#的getter / setters实际上是伪装的方法,所以读取公共字段必须比调用getter更快。

因此,请确保我进行了测试(下面的代码)。但是,如果从Visual Studio内部运行此测试,则只能产生预期的结果(即,字段比getter的速度快34%)。

从命令行运行后,它将显示几乎相同的时间...

唯一的解释可能是CLR进行了其他优化(如果我在这里错了,请纠正我)。

我不认为在实际应用中以更复杂的方式使用这些属性时,它们将以相同的方式进行优化。

请帮助我证明或否定现实生活中的属性比字段慢的想法。

问题是-我应该如何修改测试类以使CLR更改行为,以便公共领域优于吸气剂。或告诉我,任何没有内部逻辑的属性都将与字段表现相同(至少在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;
    }
}

17
鲍勃(Microsoft Bobb)非常清楚,无论他们吹嘘多少设计哲学/手法,一旦他们意识到它的运行速度提高了34%(即使在99.9%的情况下进行了微优化),每个人都会使用公共场所而不是属性。人们还是会这样做)。因此,世界的Eric Lipperts建立了一个不错的编译器和优化器,它发现您的自动属性是变相的公共领域,并进行相应的优化。
谢尔盖·卡利尼琴科

5
如果仅使用没有方法的容器,则可能要尝试使B成为结构而不是类。在Visual Studio中启动它时,还需要考虑是要执行“开始调试”还是“不进行调试而开始”。使用“开始调试”钩子可以做很多事情,使您可以执行步进,观察值等,因此对自身的性能可能会产生重大影响。
JamieSee 2012年

1
@dasblinkenlight-好点...但是看来好东西来自CLR而不是C#编译器...如果是编译器,那么它在VS中也将如此之快....我在这里缺少什么吗?
Boppity Bop 2012年

不。它不能是C#编译器,因为生成的类HAS就像您在结构中所说的那样。您可以替换它,并且使用程序必须使用新的属性设置器;)
TomTom 2012年

属性以简短形式(公共字符串Test {get; set;})编写时,具有附加的属性CompilerGeneratedAttribute。我确定编译器在扩展IL时会检查并检查是否跳过了后备字段(如果有人可以验证实际的x86程序集确实如此,那真是太棒了)。
drake7707

Answers:


62

正如其他人已经提到的,吸气剂是内联的

如果要避免内联,则必须

  • 用手动属性替换自动属性:

    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.

没有内联的吸气剂:

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

4
只是想知道,您需要使用手动属性还是将属性应用于自动属性获取器仍然有效?
2014年

是否有理由要防止内联?
Jacob Stamm's

1
它很旧,但是我仍然对@JacobStamm提出的问题感到好奇,如果有人遇到这个问题。我不确定它的范围是否足够小,可以提出自己的问题。
辛加

2
这些数字是多少?
person27年

@ person27:运行OP的代码的输出。第一个数字列是相关的列,包含传递的刻度数。越小越好(即更快)。
Heinzi

28

看看“属性与字段”-为什么如此重要?(Jonathan Aneja)来自MSDN上VB团队成员之一的博客文章。他概述了属性与字段参数,并解释了以下琐碎的属性:

我听说过在属性上使用字段的一个论点是“字段更快”,但对于琐碎的属性却不正确,因为CLR的即时(JIT)编译器将内联属性访问并生成如下代码:与直接访问字段一样高效。


嗨@Bobb,我毫不怀疑你会读。实际上,我也最喜欢海因兹的回答。我只是认为这是一个有用的小参考,并且由于该解释来自主要来源,因此总体上可以对该主题做出有益的贡献。
JamieSee 2012年

谢谢队友..这是星期五的笑话不要担心。我标记了亨氏,因为我比你早几分钟看到了他的答案。但我也喜欢你,加油!:-)
Boppity Bop 2012年

12

JIT将内联其内部度量标准确定的任何方法(不仅仅是获取方法)都将更快地内联。鉴于标准属性是return _Property;,它将在每种情况下都内联。

您看到不同行为的原因是,在连接了调试器的“调试”模式下,JIT受到严重限制,以确保任何堆栈位置都与您期望的代码匹配。

您还会忘记性能的第一法则,即测试胜过思考。例如,即使快速排序在渐近上比插入排序快,但对于极小的输入,插入排序实际上更快。


2
太好了,说了“测试胜过思考”。谢谢。
Praveen Prajapati

7

唯一的解释可能是CLR进行了其他优化(如果我在这里错了,请更正我)。

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

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

在Visual Studio中,无法在调试器中执行此操作。编译发行版,无需附加调试器即可运行,您将获得完整的优化。

我不认为在实际应用中以更复杂的方式使用这些属性时,它们将以相同的方式进行优化。

这个世界充满错觉的错觉。由于它们仍然是微不足道的,因此将对其进行优化(即,简单的代码,因此可以内联)。


谢谢...但是请删除有关调试的注释。我不是那个比较性能的调试版本的傻瓜。干杯
Boppity Bop 2012年

这是正确的,因为当您将其放入更复杂的东西中时,您会发现许多小的样式不同。用于示例的GC也不能快速清除很多东西。使引用保持更长的时间。
TomTom 2012年

4

应该注意的是,有可能在Visual Studio中看到“真实的”性能。

  1. 在启用优化的情况下以发布模式编译。
  2. 转到“调试”->“选项和设置”,然后取消选中“在模块加载时禁止JIT优化(仅受管理)”。
  3. (可选)取消选中“仅启用我的代码”,否则您可能无法进入代码。

现在,即使连接了调试器,手动组装也将是相同的,如果需要的话,您也可以加入优化的反汇编。这对于了解CLR如何优化代码至关重要。


好点子。但是它太复杂了。更容易从资源管理器启动它(尤其是因为VS在File Explorer命令中打开文件夹;)
Boppity Bop 2013年

3
但是,如果要调试并查看是否内联了某些内容,则必须将调试器作为一个单独的步骤进行附加。通过取消选中这两个选项,您只需按F5即可调试优化的构建,然后进入生成的汇编代码。
Asik

2

阅读完所有文章后,我决定使用以下代码作为基准:

    [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%.

汽车属性似乎是一种更好的方法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.