为什么默认情况下不将自动属性进行内联处理?

4
由于属性本质上是方法,因此它们可能执行的任何逻辑的性能提高与否是可以理解的,因此JIT需要检查方法是否值得内联。
然而,自动属性(就我所知)不能有任何逻辑,只是返回或设置基础字段的值。据我所知,编译器和JIT将自动属性视为任何其他方法。
值类型属性显示的行为与变量本身不同,但引用类型属性应该与直接访问基础变量具有完全相同的行为。
// Automatic Properties Example
public Object MyObj { get; private set; }
自动属性适用于引用类型时是否可能存在性能损失?如果没有,编译器或JIT为什么不会自动内联它们呢?

注意:我知道性能提升可能微不足道,特别是在JIT如果使用足够多次就很可能内联它们的情况下 - 但是尽管收益可能很小,但这似乎是一种看起来很简单的优化措施,理论上应该被引入。


1
@Yahia,我不明白你的评论与我的问题有什么关系...自动属性在IL代码中显示为普通方法,就像普通属性一样。 - Acidic
1
“自动属性在 IL 代码中显示……就像普通属性一样”:“你已经回答了自己的问题。Jitter 怎么会知道一个属性是自动属性?如果你问为什么 C# 编译器不将它们内联,那是因为那会改变代码的语义。” - phoog
@phoog我不明白为什么你要把这个问题看成非黑即白的,好像它要么必须是一个方法,要么根本不会出现在IL代码中。例如,它可以附带某种独特的签名,表明该方法“从定义上”值得内联。此外,我认为没有必要保留那些本质上没有逻辑的属性的签名。 - Acidic
@Acidic 对于你的第一个评论,是的,让IL生产者能够标记方法为值得内联可能是有意义的,但我猜测好处(减少决定是否应该内联方法所需的时间)并不超过成本(增加IL代码的大小)。此外,这是对CLR的更改,而不仅仅是对C#的更改。 - phoog
1
@Acidic 关于第二条评论,保留那些没有逻辑背后的属性签名可以减少破坏性变更:这样可以让你在这些属性后面加上逻辑而不需要消费者重新编译他们的代码。这也是首选使用属性而不是字段的原因。如果允许编译器通过将属性作为公共字段进行内联来撤销该设计,则会使自动属性的目的无效。 - phoog
显示剩余4条评论
3个回答

5

编辑:JIT编译器的工作方式与您想象的不同,这可能是您没有完全理解我上面试图传达的原因。我引用了您下面的评论:

那是另一回事,但据我所知,只有在方法被调用足够多次时才会检查它们是否值得内联。更不用说检查本身就会影响性能了。(现在让性能损失的大小无关紧要。)

首先,大多数方法都会被检查以确定它们是否可以内联。其次,请记住,方法只会被JIT一次,而且正是在那一次中,JIT编译器将确定其中任何被调用的方法是否会被内联。这可以在程序执行任何代码之前发生。什么使被调用的方法成为内联的好候选者?

x86 JIT编译器(x64和ia64不一定使用相同的优化技术)检查一些内容以确定一个方法是否是内联的好候选者,绝对不只是它被调用的次数。这篇文章列出了一些内容,例如内联是否会使代码更小,调用点是否将在循环中执行等。每个方法都是根据自己进行优化的,因此该方法可能会被内联到一个调用方法中,但在另一个调用方法中却不会,例如在循环中的情况。这些优化启发式算法仅适用于JIT,C#编译器并不知道:它正在生成IL而非本机代码。它们之间有一个巨大的区别;本机代码与IL代码大小可能会有很大的不同。
总之,C#编译器不会为了性能原因内联属性。
JIT编译器会内联大部分简单属性,包括自动属性。您可以在这篇有趣的博客文章中了解更多JIT如何决定内联方法调用的信息。
C#编译器根本不会内联任何方法。我认为这是因为CLR的设计方式。每个程序集都被设计成可在不同机器之间移植。很多时候,您可以改变.NET程序集的内部行为而无需重新编译所有代码,只需进行替换即可(至少在类型未更改时如此)。如果将代码内联,它会破坏这种优秀的设计,您将失去这种光彩。
让我们先谈论C++中的内联。(完全透明,我已经有一段时间没有全职使用C++了,所以我的解释可能含糊、生疏或者完全错误!我指望我的SOers来纠正和指责我)
C ++ 内联关键字 就像告诉编译器:“嘿,我想要内联这个函数,因为我认为它会提高性能。”不幸的是,它只是告诉编译器您更喜欢它内联; 它并没有告诉它必须这样做。

也许在早期,当编译器不如现在优化时,编译器往往会将该函数编译成内联。然而,随着时间的推移和编译器变得更加智能,编译器编写人员发现,在大多数情况下,他们比开发人员更擅长确定何时应将函数内联。对于那些不适用的情况,开发人员可以使用seriouslybro_inlineme关键字(在VC++中正式称为__forceinline)。

现在,编译器的作者为什么要这样做呢?嗯,内联函数并不总是意味着性能提升。虽然它确实可以,但如果使用不当,它也可能毁掉你程序的性能。例如,我们都知道内联代码的一个副作用是增加代码大小,或者说“肥大代码综合症”(免责声明:这不是一个真正的术语)。为什么“肥大代码综合症”是个问题呢?如果你看一下我上面链接的文章,它解释了,除其他外,内存很慢,而你的代码越大,它就越不可能适应最快的CPU缓存(L1)。最终它只能适应内存,那么内联就没有起到任何作用。然而,编译器知道这些情况何时会发生,并尽力防止它。

结合你的问题,让我们这样来看待它:C#编译器就像为JIT编译器编写代码的开发人员:JIT更加聪明(但不是天才)。它通常知道内联何时有益或有害于执行速度。"高级开发人员" C#编译器不知道内联方法调用如何有利于代码的运行时执行,因此它不会这样做。我想这实际上意味着C#编译器很聪明,因为它将优化的工作留给比它更好的人,也就是JIT编译器。


资源为什么要在方法运行未内联的情况下浪费至少“几次”,之后 JIT 开始分析以确定是否值得内联,而不是编译器提前告诉 JIT 应该内联呢? - Acidic
@Acidic,我完全同意这种情况不应该发生,幸运的是,JITer并不是这样工作的。代码只会被JIT一次,所以如果它没有被内联或者不是内联的,它将永远不会在方法调用时运行几次,然后稍后被内联。 - Christopher Currens
那是另外一回事,但据我所知,只有在方法被调用足够多次时才会检查它们是否值得内联。更不用说检查本身会对性能造成影响了。(现在先不考虑性能影响的大小。) - Acidic
这是完全正确的,但由于JITting发生在执行之前而不是同时进行,所以你失去了你的观点...所有的变化(略微)只是程序启动时间。 - Nuffin
@Tobias- JITer可能不会在开始执行之前编译您的整个程序。 它是基于类型逐个进行编译的,但即使如此,它也不会在另一个方法调用该方法之前编译静态或实例级别的方法。 - Christopher Currens

4
自动属性(据我所知)不能使用逻辑,只能获取或设置基础字段的值。就我所知,自动属性与编译器和JIT处理其他方法一样。但是,自动属性无法使用逻辑是一种实现细节,没有为编译需要特殊知识。事实上,正如您所说,自动属性被编译为方法调用。
假设自动属性已内联,并且该类和属性在不同的程序集中定义。这意味着如果属性实现更改,则必须重新编译应用程序才能看到更改。这违反了首先使用属性的目的,而应该允许您更改内部实现,而无需重新编译消费应用程序。

那么看起来它们不应该被视为普通方法。 - Acidic
@Acidic 但是C#规范明确定义属性为“类似于字段,但是字段定义了存储位置,而属性定义了访问器方法”(引用1.6.7.2节)。由于属性被定义为普通方法,为什么Jitter不应该将它们视为这样的方法呢? - phoog

2
自动属性就是那样 - 自动生成的属性get/set方法。因此,在IL中它们没有什么特别之处。 C#编译器本身只进行了很少的优化。
至于为什么不进行内联的原因 - 想象一下,您的类型在单独的程序集中,因此您可以自由地更改该程序集的源代码,使属性的get/set变得非常复杂。因此,当编译器首次看到您的自动属性并创建依赖于您的类型的新程序集时,编译器无法理解get/set代码的复杂性。
正如您在问题中已经注意到的那样 - “尤其是当JIT可能在任何情况下都内联它们时” - 这些属性方法在JIT时很可能会被内联。

你基本上只是重复了我在开头所说的。我的问题是关于为什么会这样的。 - Acidic
是的 - 因为你已经回答了自己的问题 :) -“为什么C#不做JIT会做的更可靠的事情?”-因为它不需要。查看Eric Lippert关于功能成本的任何答案,例如http://blogs.msdn.com/b/ericlippert/archive/2009/06/22/why-doesn-t-c-implement-top-level-methods.aspx。 - Alexei Levenkov
你忽略了JIT所做的事情是以更差的性能为代价的这个事实。 - Acidic
不确定我是否理解...如果JIT内联该方法,那么就好像该方法不存在一样,如果没有-为什么C#编译器会内联它呢?... - Alexei Levenkov
因为据我所知,该方法需要在“特定频率”下运行,才能使JIT考虑是否值得内联,而检查本身显然会影响性能。此外,我并没有明确表示编译器必须自行内联它。 - Acidic
1
看起来你在谈论的是 http://en.wikipedia.org/wiki/Adaptive_optimization,据我所知,这并没有被 .Net jitter(至少标准版)实现,但可以在 Java VM 中找到。.Net jitter 在方法第一次调用时完成所有工作。 - Alexei Levenkov

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