代码之家  ›  专栏  ›  技术社区  ›  Kelly Robins

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

  •  0
  • Kelly Robins  · 技术社区  · 16 年前

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

    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 回复  |  直到 16 年前
        1
  •  8
  •   Community Mohan Dere    9 年前

    据我所知,C编译器 优化这个,因为它不能确定副作用(例如,如果你有 accessCount++ 在吸气剂里?)看看这里 excellent answer by Eric Lippert

    从这个答案来看:

    c编译器从来没有做过这种优化;如前所述,这样做需要编译器对等于被调用的代码,并验证它计算的结果在被调用代码的生命周期内不会改变。C编译器不会这样做。

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

    只是一个提示,既然埃里克是C编译器团队的成员,我相信他的答案:)

        2
  •  6
  •   Eric Lippert    16 年前

    一些随意的想法。

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

    第二,回答一个表演问题的最好方法是试一试。秒表课是你的朋友。两种方法都试10亿次,看看哪个更快,然后你就会知道了。

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

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

    怎么会这样?有少量寄存器的芯片体系结构——我正在看你,x86——要求抖动非常明智,知道哪些局部变量进入寄存器,哪些进入堆栈访问。鼓励jitter将不经常使用的内容放入一个寄存器有时意味着将其他内容从该寄存器中强制出来,这将比不经常使用的值在寄存器中提供更多的好处。

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

        3
  •  3
  •   Hans Passant    16 年前

    好吧,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()中的。代码正在直接访问“速率”字段。即使calc变量不存在,引用也保存在eax寄存器中。

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

        4
  •  2
  •   Aaronaught    16 年前

    要对此进行稍微不同的调整,请考虑在将代码编译为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抖动一样棒,我只是不认为它有那么聪明。它可以内联方法,但这通常不会对性能产生很大影响。

    总之,不要依赖编译器或jitter为您进行这些优化。另外,您可以考虑遵循不在属性getter中放入昂贵计算的通用设计准则,因为 出现 给打电话的人便宜一点,尽管可能不便宜。

    如果需要性能,则在依赖字段更改时预计算这些值。或者,更好的是, 轮廓 使用如下工具的代码 EQATEC (免费)或 ANTS 看看性能成本到底在哪里。优化而不仿形就像蒙上眼罩拍摄一样。

    推荐文章