Microsoft是否建议使用适用于游戏开发的C#属性?


26

我知道有时您需要一些属性,例如:

public int[] Transitions { get; set; }

要么:

[SerializeField] private int[] m_Transitions;
public int[] Transitions { get { return m_Transitions; } set { m_Transitions = value; } }

但是我的印象是,在游戏开发中,除非您有其他理由,否则一切都应尽可能快。从我读过的书中给我的印象是Unity 3D不确定通过属性内联访问,因此使用属性会导致额外的函数调用。

我是否经常不理会Visual Studio不使用公共字段的建议?(请注意,我们会int Foo { get; private set; }在显示预期用法的地方使用优势。)

我知道过早的优化是不好的,但是正如我所说,在游戏开发中,只要有类似的努力就可以使它更快,但事情不会变慢。


34
在认为某些东西“缓慢”之前,请始终与探查器进行验证。有人告诉我“速度很慢”,分析器告诉我必须执行500,000,000次才能损失半秒。由于它在一帧中执行的次数永远不会超过几百次,因此我认为这是无关紧要的。
Almo

5
我发现这里和那里的一些抽象有助于更广泛地应用低级优化。例如,我已经看到许多使用各种链表的普通C项目,与连续数组(例如的支持ArrayList)相比,它们的速度非常慢。这是因为C没有泛型,并且这些C程序员可能发现重复重新实现链表比对ArrayListss的大小调整算法更容易。如果您提出了良好的低级优化,请对其进行封装!
hegel5000

3
@Almo您是否将相同的逻辑应用于在10,000个不同位置发生的操作?因为那是类成员访问。从逻辑上讲,在确定优化级别无关紧要之前,应将性能成本乘以10K。0.5 ms是更好的经验法则,可以缩短游戏性能,而不是半秒。您的观点仍然成立,尽管我认为正确的行动并不像您明确指出的那样明确。
piojo

5
你有两匹马。比赛他们。
桅杆

@piojo我没有。必须始终对这些事情有所思考。就我而言,它是UI代码中的for循环。一个UI实例,很少的循环,很少的迭代。记录下来,我赞成你的评论。:)
Almo

Answers:


44

但是我的印象是,在游戏开发中,除非您有其他理由,否则一切都应尽可能快。

不必要。就像在应用程序软件中一样,游戏中存在对性能至关重要的代码,而对性能至关重要的代码。

如果代码每帧执行数千次,那么这样的低级优化可能很有意义。(尽管您可能首先要先检查是否确实有必要经常调用它。最快的代码是您不运行的代码)

如果代码每隔几秒钟执行一次,那么可读性和可维护性远比性能重要。

我读到的是Unity 3D不确定通过属性进行内联访问,因此使用属性会导致额外的函数调用。

int Foo { get; private set; }可以肯定的说,使用琐碎的属性,编译器将优化属性包装器。但:

  • 如果您确实有一个实现,那么您就不能再确定了。该实现越复杂,编译器对其进行内联的可能性就越小。
  • 属性会掩盖复杂性。这是一把双刃剑。一方面,它使您的代码更具可读性和可变性。但是另一方面,另一个访问您类的属性的开发人员可能会认为他们只是在访问单个变量,而没有意识到您实际上在该属性后面隐藏了多少代码。当一个属性开始变得计算量大时,我倾向于将其重构为一个显式的GetFoo()/ SetFoo(value)方法:暗示在幕后发生了很多事情。(或者,如果我需要更加确定别人是否得到提示:CalculateFoo()/ SwitchFoo(value)

即使顶级字段是非只读类,也可以快速访问结构类型的非只读公共字段中的字段,即使该字段位于结构等内部的结构中字段或非只读的独立变量。如果适用这些条件,结构可以很大而不会对性能产生不利影响。打破这种模式将导致与结构尺寸成比例的减速。
超级猫

5
“然后,我倾向于将其重构为显式的GetFoo()/ SetFoo(value)方法:”-好一点,我倾向于将繁重的操作从属性移到方法中。
Mark Rogers

有没有办法确认Unity 3D编译器进行了您提到的优化(在IL2CPP和Android的Mono版本中)?
piojo

9
@piojo与大多数性能问题一样,最简单的答案是“随便做”。您已经准备好代码-测试具有字段和具有属性之间的区别。仅当您可以实际衡量两种方法之间的重要差异时,才需要研究实现细节。以我的经验,如果您以尽可能快的速度编写所有内容,则会限制可用的高级优化,而只是尝试争夺低级部分(“看不见树木的森林”)。例如,找到Update完全跳过的方法,比Update速度快还可以为您节省更多时间。
a安

9

如有疑问,请使用最佳做法。

您有疑问。


那是简单的答案。当然,现实更加复杂。

首先,有一个神话,游戏编程是超超高性能编程,并且一切都必须尽可能快。我将游戏编程归为性能感知编程。开发人员必须意识到性能限制并在其中进行工作。

其次,有一个神话,就是使每件事尽可能快就是使程序快速运行的原因。实际上,识别和优化瓶颈是使程序快速运行的原因,如果代码易于维护和修改,则容易/廉价/快得多。极其优化的代码很难维护和修改。因此,为了编写极其优化的程序,您必须根据需要频繁且尽可能少地使用极限代码优化。

第三,属性和访问器的好处是它们具有很强的更改能力,从而使代码更易于维护和修改-这是编写快速程序所需要的。是否想在有人要将您的值设置为null时引发异常?使用二传手。是否想在值更改时发送通知?使用二传手。是否要记录新值?使用二传手。是否想进行延迟初始化?使用吸气剂。

第四,个人而言,在这种情况下,我没有遵循微软的最佳实践。如果我需要一个包含琐碎的公共 getter和setter 的属性,则只需使用一个字段,并在需要时将其更改为一个属性即可。但是,我很少需要这种琐碎的属性-我更常见的是要检查边界条件,或者想要将设置者设为私有。


2

.net运行时很容易内联简单属性,而且通常如此。但是,此内联通常由分析器禁用,因此很容易被误导,并认为将公共财产更改为公共领域会加快软件的速度。

在其他代码调用的代码中,这些代码在重新编译时不会重新编译,因此使用属性是一个很好的情况,因为从字段更改为属性是一项重大更改。但是对于大多数代码,除了能够在get / set上设置断点之外,与公共领域相比,我从未见过使用简单属性的好处。

我在十年的职业C#程序员中使用属性的主要原因是为了阻止其他程序员告诉我我不遵守编码标准。这是一个很好的权衡,因为几乎没有理由不使用财产。


2

结构和裸字段将简化与某些非托管API的互操作性。通常,您会发现低级API希望通过引用来访问这些值,这对性能有好处(因为我们避免了不必要的复制)。使用属性是一个障碍,通常包装器库会进行复制以简化使用,有时甚至是为了安全。

因此,使用不具有属性而是裸字段的向量和矩阵类型,通常会获得更好的性能。


最佳实践并非在真空中产生。尽管有一些狂热的货物,但总的来说,最佳实践还是有充分理由的。

在这种情况下,我们有几个:

  • 属性允许您在不更改客户端代码的情况下更改实现(在二进制级别上,可以在不更改源代码级别的客户端代码的情况下将字段更改为属性,但是更改后它将编译为其他内容) 。这意味着,从一开始就使用属性,就不必重新编译引用您属性的代码,只需更改该属性在内部执行的操作即可。

  • 如果您类型的字段的所有可能值都不都是有效状态,则您不想将它们公开给客户端代码,而代码会对其进行修改。因此,如果某些值的组合无效,则希望将字段保留为私有(或内部)。

我一直在说客户代码。这意味着将调用您的代码。如果您不是在制作图书馆(甚至不是在制作图书馆,而是使用内部而不是公共的),则通常可以逃脱它,并遵守一些良好的纪律。在这种情况下,使用属性的最佳做法是防止您用脚射击。此外,如果您可以在一个文件中看到字段可以更改的所有位置,而不必担心是否需要在其他地方进行修改,则对代码进行推理就容易得多。实际上,当您找出问题出在哪里时,属性也可以放置断点。


是的,看到行业正在做的事情是有价值的。但是,您是否有动机违背最佳做法?还是您只是违背最佳实践-使得代码难以推理-仅因为其他人做到了?嗯,顺便说一下,“别人去做”就是您开始从事货物崇拜的方式。


所以...您的游戏运行缓慢吗?您最好花时间找出瓶颈并加以解决,而不是猜测可能是什么。您可以放心,编译器将进行大量优化,因此,您很可能正在寻找错误的问题。

另一方面,如果您要决定要做什么,则应先考虑哪种算法和数据结构,而不要担心字段和属性等较小的细节。


最后,您是否违背了最佳实践来赚钱?

对于您的情况(Unity和Mono for Android),Unity是否通过引用获取值?如果没有,它将以任何方式复制值,从而不会提高性能。

如果是的话,如果您要将这些数据传递给采用引用的API。将字段设为公开是否有意义,或者您可以使类型能够直接调用API?

是的,当然,可以通过使用带有裸字段的结构来进行优化。例如,您可以使用指针 Span<T>或类似指针来访问它们。它们在内存上也很紧凑,使其易于序列化以通过网络发送或放入永久存储中(是的,它们是副本)。

现在,您是否选择了正确的算法和结构,如果发现它们是瓶颈,那么您就可以确定修复该问题的最佳方法……这可能是带有裸露字段的结构。您将可以担心是否以及何时发生。同时,您可以担心更重要的事情,例如制作一款好玩的游戏或值得玩的游戏。


1

...我的印象是,在游戏开发中,除非有其他理由,否则一切都应尽可能快。

我知道过早的优化是不好的,但是正如我所说,在游戏开发中,只要有类似的努力就可以使它更快,但事情不会变慢。

您应该知道过早的优化是不好的,但这只是故事的一半(请参阅下面的完整引用)。即使在游戏开发中,并非所有事情都需要尽可能快。

首先使其工作。只有这样,才能优化需要它的位。这并不是说初稿可能会很杂乱或不必要地效率低下,但在此阶段,优化并不是优先考虑的事情。

真正的问题是程序员在错误的地方和错误的时间花了太多时间来担心效率过早的优化是编程中所有(或至少大部分)邪恶的根源。” 唐纳德·克努斯(1974)。


1

对于诸如此类的基本内容,C#优化器无论如何都将正确处理。如果您担心它(并且担心超过1%的代码,那是错的话),则会为您创建一个属性。该属性[MethodImpl(MethodImplOptions.AggressiveInlining)]大大增加了内联方法的机会(属性获取器和设置器是方法)。


0

将结构类型成员公开为属性而不是字段通常会产生较差的性能和语义,尤其是在结构实际上被视为结构而不是“古怪对象”的情况下。

.NET中的结构是通过管道方式捆绑在一起的字段的集合,可以出于某些目的将其视为一个单元。.NET Framework旨在允许使用与类对象几乎相同的方式来使用结构,有时这可能很方便。

围绕结构的许多建议是基于这样的观念,即程序员将要使用结构作为对象,而不是将它们作为磁带的字段集合。另一方面,在许多情况下,将某些行为表现为一束束缚在一起的字段可能会很有用。如果这是需要的,.NET建议将为程序员和编译器增加不必要的工作,与直接使用结构相比,产生的性能和语义更糟。

使用结构时要记住的最大原则是:

  1. 不要按值传递或返回结构,否则会导致不必要的复制。

  2. 如果坚持原则1,那么大型结构的性能将与小型结构相同。

如果Alphablob是含有26个公共的结构int命名为AZ域和属性的getter ab返回其总和ab字段,然后给定Alphablob[] arr; List<Alphablob> list;的代码int foo = arr[0].ab + list[0].ab;需要阅读领域abarr[0],但将需要阅读的所有26场list[0],即使它会忽略其中的两个。如果希望拥有一个通用的类似列表的集合,并且可以使用像这样的结构有效地工作alphaBlob,则应该使用以下方法替换索引的getter:

delegate ActByRef<T1,T2>(ref T1 p1, ref T2 p2);
actOnItem<TParam>(int index, ActByRef<T, TParam> proc, ref TParam param);

然后将调用proc(ref backingArray[index], ref param);。给定这样一个集合,如果将其替换sum = myCollection[0].ab;

int result;
myCollection.actOnItem<int>(0, 
  ref (ref alphaBlob item, ref int dest)=>dest = item,
  ref result);

这样就避免了需要复制alphaBlob不会在属性getter中使用的部分。由于传递的委托除了访问其ref参数外不访问任何其他内容,因此编译器可以传递静态委托。

不幸的是,.NET Framework并未定义使该程序正常运行所需的任何类型的委托,并且语法最终变得一团糟。另一方面,这种方法可以避免不必要的复制结构,这反过来又使对数组中存储的大型结构执行就地操作成为可能,这是访问存储的最有效方法。


嗨,您将答案发布到错误的页面了吗?
piojo

@pojo:也许我应该更清楚地表明,答案的目的是确定一种情况,即使用属性而不是字段是不好的情况,并确定在保持访问权限的同时如何实现字段的性能和语义优势。
超级猫

@piojo:我的编辑是否使事情更清晰?
超级猫

“即使将忽略其中的两个,也需要读取list [0]的所有26个字段。” 什么?你确定吗?您检查了MSIL吗?C#几乎总是通过引用传递类类型。
pjc50

@ pjc50:读取属性会按值产生结果。如果索引的getter list和for 属性的getter ab都已内联,则JITter可能会认识到只有成员ab是必需的,但我不知道实际上有任何JITter版本在执行此类操作。
超级猫
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.