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

发布于 2024-08-25 18:18:29 字数 604 浏览 13 评论 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; }}
    ....
}

显然,这通常会导致在给定公式中多次调用同一访问器。我很好奇编译器是否足够聪明,可以在不干预状态变化的情况下优化这些重复调用,或者这种风格是否会造成相当大的性能损失。

进一步阅读建议始终受到赞赏

I've found recently that for some types of financial calculations that the following pattern is much easier to follow and test especially in situations where we may need to get numbers from various stages of the computation.

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; }}
    ....
}

Obviously this often results in multiple calls to the same accessor within a given formula. I was curious as to whether or not the compiler is smart enough to optimize away these repeated calls with no intervening change in state, or whether this style is causing a decent performance hit.

Further reading suggestions are always appreciated

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

够运 2024-09-01 18:18:29

据我所知,C# 编译器不会对此进行优化,因为它无法确定副作用(例如,如果 getter 中有 accessCount++ 会怎样? )看看这里优秀埃里克·利珀特 (Eric Lippert) 的回答

从该答案中:

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

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

请注意,Eric 正在使用 C# 编译器团队,我相信他的回答:)

From what I know, the C# compiler doesn't optimize this, because it can't be certain of side-effects (e.g. what if you have accessCount++ in the getter?) Take a look here at an excellent answer by Eric Lippert

From that answer:

The C# compiler never ever does this sort of optimization; as noted, doing so would require that the compiler peer into the code being called and verify that the result it computes does not change over the lifetime of the callee's code. The C# compiler does not do so.

The JIT compiler might. No reason why it couldn't. It has all the code sitting right there. It is completely free to inline the property getter, and if the jitter determines that the inlined property getter returns a value that can be cached in a register and re-used, then it is free to do so. (If you don't want it to do so because the value could be modified on another thread then you already have a race condition bug; fix the bug before you worry about performance.)

Just a note, seeing as Eric's on the C# compiler team, I trust his answer :)

澜川若宁 2024-09-01 18:18:29

一些随意的想法。

首先,正如其他人所指出的,C# 编译器不会进行此类优化,尽管抖动是自由的。

其次,回答性能问题的最佳方法是尝试一下。秒表类是你的朋友。两种方法都尝试十亿次,看看哪一种更快;然后你就会知道。

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

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

怎么可能呢?具有少量寄存器的芯片架构(我正在看你,x86)要求抖动非常明智地判断哪些局部变量进入寄存器以及哪些变量进入堆栈访问。鼓励抖动将不经常使用的内容放入一个寄存器有时意味着将其他内容强制从该寄存器中取出,这些内容比不经常使用的值从寄存器中提供更多的好处。

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

A few random thoughts.

First, as others have noted, the C# compiler does not do this sort of optimization, though the jitter is free to do so.

Second, the best way to answer a performance question is to try it and see. The Stopwatch class is your friend. Try it a billion times both ways and see which one is faster; then you'll know.

Third, of course it makes no sense to spend time optimizing something that is already fast enough. Before you spend a lot of time benchmarking, spend some time profiling and looking for hot spots. This is unlikely to be one.

And fourth, another answer suggested storing intermediate results in a local variable. Note that doing so can in some situations make things considerably faster, and in others, can make it slower. Sometimes it is faster to recompute a result unnecessarily than to store it and look it up again when you need it.

How can that be? Chip architectures with a small number of registers -- I'm looking at you, x86 -- require the jitter to be very judicious about which locals get to be in registers and which get to be stack accesses. Encouraging the jitter to put something that is used infrequently in one register sometimes means forcing something else out of that register, something that would provide more benefit from being in a register than your infrequently used value.

In short: do not try to second-guess the jitter from your comfortable armchair; the behaviour of real-world code can be deeply counterintuitive. Make performance decisions based on realistic empirical measurements.

鸩远一方 2024-09-01 18:18:29

是的,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..

注意构造函数调用和属性获取器如何消失,它们被内联到 Main() 中。该代码直接访问 _rate 字段。即使 calc 变量消失了,引用也保存在 eax 寄存器中。

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

Right, the C# compiler doesn't make optimizations like this. But the JIT compiler certainly does. All the getters you posted are small enough to get inlined, resulting in a direct access to the field.

An example:

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

Generates:

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..

Note how both the constructor call and the property getter have disappeared, they are inlined into Main(). The code is directly accessing the _rate field. Even the calc variable is gone, the reference is held in the eax register.

The instruction at address 19 shows that more work could be done on the optimizer. Time permitting.

如梦初醒的夏天 2024-09-01 18:18:29

为了对此进行稍微不同的解释,请考虑一下,一旦代码被编译为 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 并查看性能成本是否真正在哪里。在没有分析的情况下进行优化就像蒙着眼睛进行拍摄一样。

To put a slightly different spin on this, consider that properties are really just wrappers around methods once the code is compiled to IL. So if, instead of this:

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

You had this:

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

Would you expect the compiler to optimize out the method call for you?

I'm not an expert on the jitter but I doubt that even the jitter will "cache" this; it would have to track all sorts of state and invalidate the entry when any of the dependent fields change, and as awesome as the .NET jitter is, I just don't think it's that clever. It may inline the method, but that usually won't make a huge difference performance-wise.

Bottom line, don't rely on the compiler or jitter to make these optimizations for you. Also, you might consider following the common design guideline of not putting expensive computations in property getters, because it appears to the caller to be cheap, even though it might not be.

If you need performance, then precompute these values whenever the dependent fields change. Or, better yet, profile the code using a tool like EQATEC (free) or ANTS and see if where the performance cost really is. Optimizing without profiling is like shooting with a blindfold on.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文