重复访问者调用的编译器优化

时间:2010-03-23 02:09:58

标签: c# optimization compiler-construction accessor

我最近发现,对于某些类型的财务计算,以下模式更容易遵循和测试,尤其是在我们可能需要从计算的各个阶段获取数字的情况下。

public class nonsensical_calculator
{ 

   ...

    double _rate;
    int _term;
    int _days;

    double monthlyRate { get { return _rate / 12; }}

    public double days { get { return (1 - i); }}
    double ar   { get { return (1+ days) /(monthlyRate  * days)
    double bleh { get { return Math.Pow(ar - days, _term)
    public double raar { get { return bleh * ar/2 * ar / days; }}
    ....
}

显然,这通常会导致在给定公式中多次调用相同的访问器。我很好奇编译器是否足够聪明,能够优化掉这些重复调用,而不会对状态进行干预,或者这种风格是否会对性能造成影响。

总是赞赏进一步的阅读建议

4 个答案:

答案 0 :(得分:8)

据我所知,C#编译器没有优化这一点,因为它不能确定副作用(例如,如果你在getter中有accessCount++怎么办? )看看excellent answer by Eric Lippert

从那个答案:

  

C#编译器从未进行过这种优化;如上所述,这样做将要求编译器对应于被调用的代码,并验证其计算的结果在被调用者代码的生命周期内不会发生变化。 C#编译器没有这样做。

     

JIT编译器可能会。没有理由不能。它的所有代码就在那里。内联属性getter是完全自由的,如果抖动确定内联属性getter返回一个可以缓存在寄存器中并重新使用的值,那么它可以自由地执行。 (如果您不希望它这样做,因为可以在另一个线程上修改该值,那么您已经遇到了竞争条件错误;在您担心性能之前修复该错误。)

请注意,在C#编译器团队中看到Eric,我相信他的答案:)

答案 1 :(得分:6)

一些随意的想法。

首先,正如其他人所指出的那样,C#编译器不会进行这种优化,尽管抖动可以自由地进行。

其次,回答性能问题的最佳方法是尝试并查看。秒表课是你的朋友。两种方式尝试十亿次,看看哪一种更快;那你就知道了。

第三,当然,花时间优化已经足够快的东西是没有意义的。在花费大量时间进行基准测试之前,花一些时间进行分析并寻找热点。这不太可能是一个。

第四,另一个答案建议将中间结果存储在局部变量中。请注意,这样做可以在某些情况下使事情变得更快,而在其他情况下,可以使速度变慢。 有时,不必要地重新计算结果比存储它更快,并在需要时再次查找。

怎么会这样?具有少量寄存器的芯片架构 - 我正在看着你,x86--要求抖动非常明智地了解哪些本地变为寄存器,哪些是堆栈访问。鼓励抖动将不经常使用的东西放在一个寄存器中有时意味着从该寄存器中强制使用其他东西,这可以从寄存器中获得比不经常使用的值更多的好处。

简而言之:不要试图从舒适的扶手椅中猜出抖动;现实世界代码的行为可能会违反直觉。根据实际的经验测量做出绩效决策。

答案 2 :(得分:3)

是的,C#编译器不会像这样进行优化。但JIT编译器确实如此。您发布的所有getter都足够小,可以内联,从而可以直接访问该字段。

一个例子:

static void Main(string[] args) {
  var calc = new nonsensical_calculator(42);
  double rate = calc.monthlyRate;
  Console.WriteLine(rate);
}

生成:

00000000  push        ebp                          ; setup stack frame
00000001  mov         ebp,esp 
00000003  sub         esp,8 
00000006  mov         ecx,349DFCh                  ; eax = new nonsensical_calculator
0000000b  call        FFC50AD4 
00000010  fld         dword ptr ds:[006E1590h]     ; st0 = 42
00000016  fstp        qword ptr [eax+4]            ; _rate = st0
00000019  fld         qword ptr [eax+4]            ; st0 = _rate
0000001c  fdiv        dword ptr ds:[006E1598h]     ; st0 = st0 / 12
00000022  fstp        qword ptr [ebp-8]            ; rate = st0
      Console.WriteLine(rate);
// etc..

注意构造函数调用和属性getter都已消失,它们被内联到Main()中。代码直接访问_rate字段。即使calc计算变量消失了,引用仍保存在eax寄存器中。

地址19处的指令表明可以在优化器上完成更多工作。时间允许。

答案 3 :(得分:2)

要对此进行稍微不同的旋转,请考虑一旦代码编译为IL,属性实际上只是方法的包装。所以,如果,而不是:

public class nonsensical_calculator
{
    double bleh
    {
        get { return Math.Pow(ar - days, _term); }
    }
    // etc.
}

你有这个:

public class nonsensical_calculator
{
    double GetBleh()
    {
        return Math.Pow(ar - days, _term);
    }
}

您是否希望编译器为您优化方法调用?

我不是抖动的专家,但我怀疑即使是抖动也会“缓存”这个;当任何依赖字段发生变化时,它必须跟踪各种状态并使条目无效,并且像.NET抖动一样令人敬畏,我只是觉得它不那么聪明。它可能会内联该方法,但通常不会在性能方面产生巨大差异。

最重要的是,不要依赖编译器或抖动来为您进行这些优化。此外,您可能会考虑遵循通常的设计准则,即不将昂贵的计算放在属性获取器中,因为它对调用者来说是便宜的,即使它可能不是。

如果需要性能,则只要依赖字段发生更改,就会预先计算这些值。或者,更好的是,使用EQATEC(免费)或ANTS等工具配置文件代码,并查看性能成本的确实位置。没有剖析的优化就像用眼罩拍摄一样。