由于缓存委托,C#编译器的行为异常 [英] Weird behaviour of c# compiler due caching delegate

查看:52
本文介绍了由于缓存委托,C#编译器的行为异常的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设我有以下程序:

  static void SomeMethod(Func< int,int> otherMethod)
{
otherMethod(1);
}

static int OtherMethod(int x)
{
return x;
}

static void Main(string [] args)
{
SomeMethod(OtherMethod);
SomeMethod(x => OtherMethod(x));
SomeMethod(x => OtherMethod(x));
}

我无法理解已编译的il代码(它使用了过多的代码)。这是简化版本:

  class C 
{
public static C c;
public static Func< int,int> foo;
public static Func< int,int> foo1;
静态C()
{
c =新的C();
}
C(){}
public int b(int x)
{
return OtherMethod(x);
}
public int b1(int x)
{
return OtherMethod(x);
}
}

静态void Main()
{
SomeMethod(new Func< int,int>(OtherMethod));
if(C.foo!= null)
SomeMethod(C.foo)
else
{
C.foo = new Func< int,int>(c ,Cb)
SomeMethod(C.foo);
}
if(C.foo1!= null)
SomeMethod(C.foo1)
else
{
C.foo1 = new Func< int ,int>(c,C.b1)
SomeMethod(C.foo1);
}
}

为什么编译器会创建非静态的相等方法 b / b1 ?相等意味着它们具有相同的代码

解决方案

您的问题是:为什么编译器没有意识到这两行

  SomeMethod(x => OtherMethod(x)); 
SomeMethod(x => OtherMethod(x));

相同,写为

  if(未创建委托)
创建委托并将其存放
SomeMethod(the委托);
SomeMethod(代表);

?好吧,让我用几种方式回答这个问题。



首先,编译器是否被允许进行优化?是。该规范指出,允许C#编译器 将两个完全相同的lambda放入单个委托中。实际上,您可以看到它已经在某种程度上进行了优化:它创建了每个委托一次并将其保存下来,这样就不必在以后再次调用代码时再次创建它。请注意,在仅调用一次代码的情况下,这是浪费内存。



第二,需要编译器 进行缓存优化?否。该规范指出,编译器仅允许进行进行优化,而无需进行进行优化。



是否需要编译器 来进行所需的优化?显然不是,因为事实并非如此。它是被允许的,也许将来的编译器版本也可以。编译器是开源的。如果您关心此优化,请编写并提交拉取请求。



第三,可能进行所需的优化吗?是。编译器可以采用同一方法出现的所有成对的lambda,将它们编译为内部树格式,并进行树比较以查看它们是否具有相同的内容,然后为两者生成相同的静态后备字段。 p>

因此,现在我们遇到了一个情况:允许编译器进行特定的优化,而事实并非如此。您问为什么不呢?这是一个容易回答的问题:只有在某人花费大量时间和精力来完成所有优化之后,




  • 仔细设计优化:究竟触发和不触发优化的条件是什么?优化应该有多普遍?您建议他们检测类似的lambda尸体,但为什么要停在那里?您有两个相同的语句代码,那么为什么不为这些语句生成代码一次而不是两次?如果您有重复的一组语句怎么办?在这里要做大量的设计工作。

  • 特别地,设计的一个重要方面是:用户可以在不保留代码的情况下合理地手工进行优化吗?可读的。在这种情况下,是的,他们可以轻松实现。只需将重复的lambda分配给变量,然后使用该变量。自动执行关心的用户可以轻松完成的操作的优化并不是一个非常有趣或引人注目的优化。

  • 您的示例很简单;实际代码不是。您建议的设计如何使用相同的嵌套 lambda?等等。

  • 您的优化是否会使调试器中的代码行为看起来很奇怪?您可能已经注意到,当调试在启用优化的情况下编译的代码时,调试器的行为似乎很奇怪。那是因为在生成的代码和原始代码之间不再存在清晰的映射。您的优化会使情况变得更糟吗?用户可以接受吗?调试器是否需要了解优化?如果是这样,则必须更改调试器。在这种情况下,也许不是,但这是您必须要回答的问题。

  • 请专家评审设计;这会占用他们的时间,并且可能会导致设计更改

  • 对优化的优缺点进行估算-优化通常会隐藏成本,例如我提到的内存泄漏之前。特别是,优化常常排除可能会更好的其他优化。

  • 对这种优化在全球范围内的总节省进行估算。优化实际上会影响实际代码吗?它会更改该代码的正确性吗?全球是否有任何生产代码会因这种优化而中断,并导致X公司的CTO致电Microsoft的CTO要求修复?如果答案是肯定的,那么也许您可能不想进行此优化。 C#不是玩具。每天都有成千上万的人依靠它的正确操作。

  • 编译时间上进行优化的估计负担是多少?无需在两次击键之间进行编译,但编译必须非常快。在编译器的公共代码路径中引入超线性算法的任何事情都是不可接受的。您可以实现优化以使其代码大小线性吗?请注意,我之前草拟的算法-比较所有对-代码大小是超线性的。 (练习:在所有成对的lambda上进行树比较的最坏情况下的渐近性能是什么?)

  • 实际上实现了优化。我鼓励您这样做。

  • 测试优化;它实际上会产生更好的代码吗?以什么度量标准?不更改任何度量标准的优化不是优化。

  • 注册以永久修复优化中的错误。



您想要的优化根本无法满足要求。没有人会这样写代码。如果他们这样做了,并且他们担心它会复制一个对象,那么他们可以轻松地自己修复它。因此,优化程序会优化不存在的代码,以获取胜利,即在程序将分配的数百万个对象中构建单个对象。不值得。



但是,如果您认为确实如此,请继续执行并提交请求。确保提交我上面提到的调查结果,因为这些是真正的工作所在。通常,实现是功能花费在功能上的最小部分。这就是为什么C#是成功的语言。


Suppose I have following program:

static void SomeMethod(Func<int, int> otherMethod)
{
    otherMethod(1);
}

static int OtherMethod(int x)
{
    return x;
}

static void Main(string[] args)
{
    SomeMethod(OtherMethod);
    SomeMethod(x => OtherMethod(x));
    SomeMethod(x => OtherMethod(x));
}

I cannot understand compiled il code (it uses too extra code). Here is simplified version:

class C
{
    public static C c;
    public static Func<int, int> foo;
    public static Func<int, int> foo1;
    static C()
    {
        c = new C();
    }
    C(){}
    public int b(int x)
    {
        return OtherMethod(x);
    }
    public int b1(int x)
    {
        return OtherMethod(x);
    }
}

static void Main()
{
    SomeMethod(new Func<int, int>(OtherMethod));
    if (C.foo != null)
        SomeMethod(C.foo)
    else
    {
        C.foo = new Func<int, int>(c, C.b)
        SomeMethod(C.foo);
    }
    if (C.foo1 != null)
        SomeMethod(C.foo1)
    else
    {
        C.foo1 = new Func<int, int>(c, C.b1)
        SomeMethod(C.foo1);
    }
}

Why does compiler create not static equal methods b/b1? Equal means that they have the same code

解决方案

Your question is: why did the compiler not realize that the two lines

SomeMethod(x => OtherMethod(x));
SomeMethod(x => OtherMethod(x));

Are the same and write this as

if ( delegate is not created ) 
  create the delegate and stash it away
SomeMethod( the delegate );
SomeMethod( the delegate );

? Well let me answer that question in several ways.

First off, is the compiler permitted to make that optimization? Yes. The specification calls out that a C# compiler is permitted to make two lambdas that do exactly the same thing into a single delegate. And in fact you can see that it already does this optimization in part: it creates each delegate once and saves it away so that it doesn't have to create it again later when the code is called again. Notice that this is a waste of memory in the case where code is only called once.

Second, is the compiler required to make the caching optimization? No. The specification calls out that the compiler is only permitted to make the optimization, but not required to.

Is the compiler required to make the optimization you want? Obviously not, because it doesn't. It is permitted to, and maybe a future version of the compiler will. The compiler is open-source; if you care about this optimization, go write it and submit a pull request.

Third, is it possible to make the optimization you want? Yes. The compiler could take all pairs of lambdas that appear in the same method, compile them to the internal tree format, and do a tree comparison to see if they have the same content, and then generate the same static backing field for both.

So now we have a situation: the compiler is permitted to make a particular optimization, and it doesn't. And you've asked "why not"? That's an easy question to answer: all optimizations are not implemented until someone spends the considerable time and effort to:

  • Carefully design the optimization: under precisely what conditions is the optimization triggered and not triggered? How general should the optimization be? You've suggested that they detect similar lambda bodies but why stop there? You have two identical statements of code, so why not generate the code for those statements once instead of twice? What if you had a repeated group of statements? There is a huge amount of design work to do here.
  • In particular, an important aspect of the design is: could the user reasonably do the optimization "by hand" while still keeping the code readable. In this case, yes they could, easily. Just assign the duplicated lambda to a variable and then use the variable. An optimization which does automatically something that a user who cared could have done themselves easily is not really a very interesting or compelling optimization.
  • Your examples are trivial; real-world code is not. What does your proposed design do with identical nested lambdas? And so on.
  • Does your optimization cause the behaviour of the code in the debugger to "look weird"? You have probably noticed that when debugging code that was compiled with optimizations turned on, the debugger seems to behave weirdly; that's because there's no longer a clear mapping between the generated code and the original code. Does your optimization make that worse? Is it acceptable to users? Does the debugger need to be aware of the optimization? If so, you'll have to change the debugger. In this case, probably not, but these are questions you have to ask and answer.
  • Get the design reviewed by experts; this takes up their time, and will likely result in changes to the design
  • Make estimates of the pros and cons of the optimization -- optimizations often have hidden costs, like the memory leak I mentioned before. In particular, optimizations often preclude other optimizations which might be better.
  • Make estimates as to the total savings world-wide of this optimization. Does the optimization actually affect real-world code? Does it change the correctness of that code? Is there any production code, anywhere in the world, that would break with this optimization and cause the CTO of company X to call the CTO of Microsoft demanding a fix? If the answer is yes then maybe you might want to not do this optimization. C# is not a toy. Millions and millions of people depend on its correct operation every day.
  • What's the estimated burden of doing the optimization on compile time? Compilation doesn't have to happen between keystrokes but it does have to be pretty fast. Anything which introduces a superlinear algorithm in a common code path in the compiler is going to be unacceptable. Can you implement your optimization so that it is linear in code size? Note that the algorithm I sketched before -- compare all pairs -- is superlinear in code size. (Exercise: what's the worst case asymptotic performance of doing a tree comparison on all pairs of lambdas?)
  • Actually implement the optimization. I encourage you to do so.
  • Test the optimization; does it actually produce better code? On what metric? An optimization which causes no change to any metric is not an optimization.
  • Sign up to fix bugs in the optimization forever.

The optimization you want simply doesn't meet the bar. No one writes code like that. If they did, and they cared that it duplicated an object, they could easily fix it themselves. So the optimization optimizes code that doesn't exist, in order to get a "win" that is the construction of a single object amongst the millions and millions of objects the program will allocate. Not worth it.

But again, if you think it is, go ahead and implement it and submit a pull request. Make sure to submit the results of the investigations I noted above, because those are where the real work is. The implementation is usually the smallest part of the total effort spent on a feature; that's why C# is a successful language.

这篇关于由于缓存委托,C#编译器的行为异常的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆