0

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

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

4 回答 4

8

据我所知,C# 编译器并没有对此进行优化,因为它不能确定副作用(例如,如果你accessCount++在 getter 中有什么?)看看这里Eric Lippert 的一个很好的答案

从那个答案:

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

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

只是一个注释,作为 C# 编译器团队的 Eric,我相信他的回答 :)

于 2010-03-23T02:13:52.573 回答
7

一些随意的想法。

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

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

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

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

这个怎么可能?具有少量寄存器的芯片架构——我在看你,x86——要求抖动非常明智地决定哪些本地人进入寄存器,哪些是堆栈访问。鼓励抖动将不经常使用的东西放在一个寄存器中有时意味着将其他东西从该寄存器中取出,这会比您不经常使用的值提供更多的好处。

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

于 2010-03-23T04:55:17.800 回答
3

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

一个例子:

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 处的指令表明可以在优化器上做更多的工作。时间允许。

于 2010-03-23T02:52:18.923 回答
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 抖动一样棒,我只是不认为它那么聪明。它可能会内联该方法,但这通常不会对性能产生巨大影响。

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

如果您需要性能,请在相关字段更改时预先计算这些值。或者,更好的是,使用EQATEC(免费)或ANTS之类的工具分析代码,看看性能成本到底在哪里。没有配置文件的优化就像戴着眼罩拍摄一样。

于 2010-03-23T02:27:30.783 回答