.net C#编译器是否足够聪明,可以优化这些代码?

8e2ybdfx  于 2023-06-25  发布在  .NET
关注(0)|答案(7)|浏览(101)

在这个问题中请忽略代码可读性。
在性能方面,下面的代码应该这样写:

int maxResults = criteria.MaxResults;

if (maxResults > 0)
{
    while (accounts.Count > maxResults)
        accounts.RemoveAt(maxResults);
}

或者像这样:

if (criteria.MaxResults > 0)
{
    while (accounts.Count > criteria.MaxResults)
        accounts.RemoveAt(criteria.MaxResults);
}


编辑:criteria是一个class,而MaxResults是一个简单的整数属性(即public int MaxResults { get { return _maxResults; } }
C#编译器是否将MaxResults视为一个黑盒子,并每次都对其进行评估?或者它是否足够聪明,能够计算出我对同一个属性进行了3次调用,而在调用之间没有修改该属性?如果MaxResults是一个字段呢?
优化的法则之一是预先计算,所以我本能地编写了与第一个清单类似的代码,但我很好奇这种事情是否会自动为我完成(再次忽略代码可读性)。
(Note:我对听到“微优化”的论点不感兴趣,这在我发布的特定案例中可能是有效的。我只想知道正在发生或不发生的事情背后的一些理论。

vi4fp9gy

vi4fp9gy1#

首先,真正回答性能问题的唯一方法是实际尝试两种方法,并在现实条件下测试结果。
也就是说,其他的答案说“编译器”不做这种优化,因为属性可能有副作用,这是正确的也是错误的。这个问题的问题(除了基本问题,即如果不实际尝试并测量结果,就无法回答)是,“编译器”实际上是两个编译器:C#编译器(编译为MSIL)和JIT编译器(将IL编译为机器码)。
C#编译器从来没有做过这种优化;如上所述,这样做将要求编译器对等到被调用的代码中,并验证其计算的结果在被调用者代码的生命周期内没有改变。C#编译器不会这样做。
JIT编译器可能。没有理由不能。所有的代码都在那里。内联属性getter是完全自由的,如果抖动确定内联属性getter返回一个可以缓存在寄存器中并重用的值,那么它可以自由地这样做。(如果你不希望它这样做,因为这个值可能会在另一个线程上被修改,那么你已经有了竞争条件bug;在你担心性能之前先修复bug。)
我不知道jitter是否真的 * 做了 * 内联属性获取,然后注册值。我对抖动一无所知。但如果它认为合适,它是允许这样做的。如果你想知道它是否这样做,你可以(1)询问编写jitter的团队中的某个人,或者(2)在调试器中检查jitter代码。
最后,让我借此机会指出,计算一次结果,存储结果并重复使用它 * 并不总是一个优化 *。这是一个令人惊讶的复杂问题。有各种各样的事情需要优化:

  • 执行时间
  • 可执行代码的大小--这对可执行时间有很大的影响,因为大的代码需要更长的时间来加载,增加了工作集的大小,给处理器缓存、RAM和页面文件带来压力。从长远来看,在启动时间和缓存局部性等重要指标上,小的慢代码通常比大的快代码快。
  • 寄存器分配--这也对执行时间有重大影响,特别是在像x86这样具有少量可用寄存器的体系结构中。注册一个值以供快速重用可能意味着有更少的寄存器可用于需要优化的其他操作;也许优化这些操作反而会是一个净收益。
  • 等等。很快就真实的复杂。

简而言之,您不可能知道编写代码来缓存结果而不是重新计算结果实际上是(1)更快,还是(2)更好的性能。* 更好的性能并不总是意味着使特定例程的执行更快。* 更好的性能是弄清楚哪些资源对用户是重要的-执行时间,内存,工作集,启动时间,等等-并优化这些事情。如果没有(1)与客户交谈,了解他们关心的是什么,(2)实际测量,看看您的变化是否在预期的方向上产生了可衡量的效果,就无法做到这一点。

wz1wpwve

wz1wpwve2#

如果MaxResults是一个属性,那么它不会优化它,因为getter可能有复杂的逻辑,比如:

private int _maxResults;
public int MaxReuslts {
  get { return _maxResults++; }
  set { _maxResults = value; }
}

看看如果它嵌入到代码中,行为会如何改变?
如果没有逻辑……你写的两种方法都很好,这是一个非常微小的差异,所有关于它的可读性对你(或你的团队)…你是看它的人。

l2osamch

l2osamch3#

您的两个代码示例只能保证在单线程环境中具有相同的结果,而.Net则不是,并且如果MaxResults是一个字段(不是属性)。除非使用同步特性,否则编译器不能假设criteria.MaxResults在循环过程中不会改变。如果它是一个属性,它不能假设使用该属性没有副作用。
Eric Lippert非常正确地指出,这在很大程度上取决于你所说的“编译器”是什么意思。C# -> IL编译器?还是IL ->机器码(JIT)编译器?而且他正确地指出,JIT很可能能够优化属性getter,因为它拥有所有的信息(而C# -> IL编译器不一定)。它不会改变多线程的情况,但这是一个很好的观点。

mwecs4sa

mwecs4sa4#

每次都会调用和评估它。编译器无法确定一个方法(或getter)是否是确定性的和纯的(没有副作用)。
请注意,属性的实际计算可能由JIT编译器内联,使其与简单字段一样快。
使资产评估成为一项廉价的操作是一种好的做法。如果在getter中进行了一些繁重的计算,请考虑手动缓存结果,或者将其更改为方法。

rwqw0loc

rwqw0loc5#

为什么不测试一下呢?
只要设置2个控制台应用程序,让它看起来1000万次,并比较结果...请记住,将它们作为正确发布的应用程序运行,这些应用程序已经正确安装,否则您无法保证您不仅仅是在运行MSIL。
实际上,你可能会得到大约5个答案,说“你不应该担心优化”。它们显然不会编写在可读之前需要尽可能快的例程(例如游戏)。
如果这段代码是执行数十亿次的循环的一部分,那么这种优化可能是值得的。例如,max结果可能是一个重写的方法,因此您可能需要讨论虚方法调用。
回答这些问题的唯一方法是弄清楚这是一段将从优化中受益的代码。然后你需要知道哪些事情会增加执行时间。实际上,我们这些凡人无法先验地做到这一点,所以必须简单地尝试2-3个不同版本的代码,然后测试它。

lokaqttq

lokaqttq6#

如果criteria是一个类类型,我怀疑它是否会被优化,因为在此期间,另一个线程总是可以更改该值。对于struct,我不确定,但我的直觉是它不会被优化,但我认为无论如何,在这种情况下,它不会在性能上有太大的差异。

3wabscal

3wabscal7#

每个人都给出了非常学术和理论的答案。更糟糕的是,有些人声称的东西,我真的很想看到一个现实生活的例子。(比如说有时候第二个代码会运行得更快。
这是你从我在C#中的类似经历中得到的真实答案。第一个代码每次测试这种类型的示例时都跑得更快,这让我认为C#很少或从未优化过这种类型的东西。即使在不使用getter时也是如此;即直接访问成员时。(我经常测试这类东西,因为我喜欢性能调整)。获得会员越困难,就越重要。例如:

int maxResults = simulations[name].getMatrix(id).criteria.MaxResults;

一般来说,根据我的经验,如果你不确定C#编译器会优化某些东西,它不会。这与C++(非托管)编译器相反,它可以优化真正疯狂的东西。

***旁注:***在我看来,第一个选项在其他方面也更好,比如代码大小、寄存器缓存和代码可读性。所以这真的很简单。

相关问题