.NET中属性的性能开销

22

我在某个地方读到,拥有公共属性比拥有公共成员更可取。

  1. 这只是因为抽象和模块化吗?还有其他的主要原因吗?

  2. 编译器将属性访问转换为函数调用。对于没有备份存储的属性(例如public string UserName { get; set; }),与直接成员访问相比,性能开销会有多大?(我知道通常不会有什么区别,但在我的某些代码中,属性被访问数百万次。)

编辑1: 我运行了一些关于整数成员和属性的测试代码,公共成员大约比属性快3-4倍。(在Debug中,公共成员的时间是57 ms,而属性的时间是206 ms;在发布版中,时间分别为57和97,这是最常见的运行值)。对于1000万次读写操作,两者都很小,不足以证明任何更改。

代码:

    class TestTime1
{
    public TestTime1() { }
    public int id=0;
}
class TestTime2
{
    public TestTime2() { }
    [DefaultValue(0)]
    public int ID { get; set; }
}


class Program
{
    static void Main(string[] args)
    {
        try
        {
            TestTime1 time1 = new TestTime1();
            TestTime2 time2 = new TestTime2();
            Stopwatch watch1 = new Stopwatch();
            Stopwatch watch2 = new Stopwatch();
            watch2.Start();
            for (int i = 0; i < 10000000; i++)
            {
                time2.ID = i;
                i = time2.ID;
            }
            watch2.Stop();
            watch1.Start();
            for (int i = 0; i < 10000000; i++)
            {
                time1.id = i;
                i = time1.id;
            }
            watch1.Stop();
            Console.WriteLine("Time for 1 and 2 : {0},{1}",watch1.ElapsedMilliseconds,watch2.ElapsedMilliseconds);

        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
        Console.In.ReadLine();
    }
}

序列化和数据绑定是我能想到的原因。 - LiFo
5
优化前的调试版本的因素差异并不重要,因为您不会将调试版本发送给客户。此外,我注意到在您的测试中,您正在测量“属性的jit时间”以及“访问时间”。如果您关心jit属性的摊销成本(包括jit启动时间),那么这是一回事。但是,如果您关心的是每次使用的成本,则不要像您在这里所做的那样混淆jit成本和每次使用成本。而且无论您的测量技术如何:优化“最慢的内容”。我怀疑这不是它。 - Eric Lippert
9个回答

21

在 Release 构建中运行测试 20 次,确保启用 JIT 优化:

Time for 1 and 2 : 47,66
Time for 1 and 2 : 37,42
Time for 1 and 2 : 25,36
Time for 1 and 2 : 25,25
Time for 1 and 2 : 27,25
Time for 1 and 2 : 25,25
Time for 1 and 2 : 26,25
Time for 1 and 2 : 25,25
Time for 1 and 2 : 25,25
Time for 1 and 2 : 25,25
Time for 1 and 2 : 25,25
Time for 1 and 2 : 25,25
Time for 1 and 2 : 25,25
Time for 1 and 2 : 25,25
Time for 1 and 2 : 25,25
Time for 1 and 2 : 25,25
Time for 1 and 2 : 25,25
Time for 1 and 2 : 25,25
Time for 1 and 2 : 25,25
Time for 1 and 2 : 25,25

没错,JIT编译器在内联属性访问器方面非常出色。性能不是问题,永远不应该考虑。


嗯...我在调试模式下进行测试,而不是发布模式。我的错! - apoorv020
在发布版本中,我的机器仍然有80%的开销。我的机器是32位的,我发现32位和64位之间存在一些奇怪的性能差异。 - apoorv020
3
我的也是32位的,我猜我们使用同一个JIT编译器。 在“工具+选项”中,选择“调试”,然后选择“常规”,取消勾选“在模块加载时抑制JIT优化”。 - Hans Passant

18

不必担心性能开销。它非常小,您不应考虑削弱类的封装性; 这将是最糟糕的过早优化。


9
这是因为抽象和模块化吗?还有其他的主要原因吗?
据我所知,这些原因本身就足够有说服力了。但也许其他人会对此进行补充。
编译器将属性访问转换为函数调用。对于没有备份存储的属性(例如public string UserName { get; set; }),与直接成员访问相比,性能开销会有多大?(我知道通常不应该有区别,但在我的某些代码中,属性被访问数百万次。)
在生成的中间语言中,属性访问被转换为方法调用。然而,正如其名称所示,这只是一个中间语言:它会被即时编译成其他东西。这个转换步骤还涉及优化,比如简单属性访问器的内联。
我希望(但需要测试以确保)JITter会处理这样的访问器,因此不应该有性能差异。

1
这里有一个原因:.NET标准规定,除了极少数例外情况,您不能将字段公开为public。至于内联,请参见我对Adam的评论。 - Steven Sudit
10
标准有其存在的理由,我认为我们需要找到这个理由,而不是只因为“微软这么说”。 - Thomas
我会强调(例如加粗)内联,因为据我所知,这适用于Mono和CLR上的所有属性。 - Dykam
@Thomas:是的,绝对没错!我并不是在暗示这个标准是武断或者无益的。我只是想说,它本身作为一个标准就足以成为一个好理由,前提是其他条件(或多或少)相等的情况下,我们应该按照这个标准去做。既然性能考虑并不重要,那么我认为模糊的“其他条件相等”准则已经得到满足了。 :-) - Steven Sudit
@Dykam:我不会这样做,因为与封装的损失相比,速度差异并不显著,并且内联仅限于调用方位于同一程序集的情况。 - Steven Sudit
2
以下是关于内联问题的更权威的回答(附有链接):https://dev59.com/9HRB5IYBdhLWcg3wXWK2#646780 - Steven Sudit

4

这主要是为了抽象化的目的(您可以稍后添加验证而不会破坏现有代码或需要重新编译)。

即使使用自动属性,编译器仍然会生成一个后备字段,并且将按此方式执行。


3

请确保使用Ctrl-F5而不是F5来运行程序;否则,调试器仍然会附加到程序上,某些优化可能无法正常工作,即使在发布模式下也是如此。至少在我的计算机上是这样的:F5给出了类似于你贴出的结果,而Ctrl-F5则给出了相同的结果。


1

1) 这是关于封装原则的,但其他 .NET 功能使用属性,如数据绑定。

2) 我不确定我同意这个观点, 我一直听说如果属性是一个简单的 get/set,它的速度就像标准字段访问一样快 - 编译器会为你做这个。

更新: 看起来有点两者兼备,编译成方法调用,但 JIT 优化掉了。无论如何,这种性能问题不会对您的代码产生实质性影响。但请注意,实现属性的指导方针是使它们尽可能轻量级,调用者不希望它们很昂贵。


这个问题还有更多的细节。我认为只有当调用方和属性在同一个程序集中定义时,才可以内联调用。但是,实际上这并不重要。与能够使用尽可能多的逻辑替换基础字段相比,调用开销微乎其微。 - Steven Sudit
奇怪,这是一个不内联的相当奇怪的细节 - 在某种程度上令人失望哈哈。正如提到的那样,无论是编译消除还是不消除,性能损害都是可以忽略的。 - Adam Houldsworth
关于性能无关性的问题,我同意,但我认为这种限制有一定的道理。当为属性调用生成CIL代码时,编译器需要访问实现,以便确认只有对后备变量的赋值,并且可以识别该变量。换句话说,这种优化是原始编译的一部分,而不是最终的JIT编译。 - Steven Sudit
啊,是的,那确实有道理。 - Adam Houldsworth
冒昧重复一下,这里有一个更权威的答案链接:https://dev59.com/9HRB5IYBdhLWcg3wXWK2#646780 - Steven Sudit

0

在我发布了this篇文章之后,我意识到它基本上是为了隐藏对象的内部工作原理。


0

我之前已经问过同样的问题

我猜你正在使用VS2008,使用64位操作系统,并将编译设置为“任何CPU”?如果是这样,在x64 JIT编译器中,属性不会被内联。在32位上,它们与公共字段的性能相同,因为它们会被内联。


0
如果你想要一个具体的例子来说明为什么需要属性而不能使用普通成员变量,那就考虑一下继承:如果一个类使用了公共成员变量,那么派生类就无法实现验证或其他getter/setter行为。他们只能使用变量本身,如果他们想要做些不同的事情,就必须 1)忽略现有的成员变量并创建一个新属性,2)添加一个新属性,并且3)覆盖每个调用或依赖于成员变量的方法以使用属性。这不仅是不必要的额外工作,如果编写派生类的人没有访问源代码,这可能几乎是不可能的。
如果基类使用属性而不是成员变量,只需要在get/set函数中添加验证或其他行为即可完成。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接