由于缓存委托,c#编译器的奇怪行为

LmT*_*oon 9 c# clr compiler-optimization

假设我有以下程序:

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));
}
Run Code Online (Sandbox Code Playgroud)

我无法理解编译的il代码(它使用了额外的代码).这是简化版:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Eri*_*ert 18

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

SomeMethod(x => OtherMethod(x));
SomeMethod(x => OtherMethod(x));
Run Code Online (Sandbox Code Playgroud)

是相同的,写这个

if ( delegate is not created ) 
  create the delegate and stash it away
SomeMethod( the delegate );
SomeMethod( the delegate );
Run Code Online (Sandbox Code Playgroud)

?让我以几种方式回答这个问题.

首先,允许编译器进行优化吗?是.该规范要求允许 C#编译器将两个lambdas做成完全相同的事情到一个委托中.实际上你可以看到它已经部分地进行了这种优化:它创建了每个委托一次并将其保存起来,以便以后再次调用代码时不必再创建它.请注意,在仅调用一次代码的情况下,这会浪费内存.

第二,编译器是否需要进行缓存优化?不是.规范要求编译器只允许进行优化,但不是必需的.

编译器是否需要进行所需的优化?显然不是,因为它没有.它是允许的,也许是编译器的未来版本.编译器是开源的; 如果您关心此优化,请编写并提交拉取请求.

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

所以现在我们有一种情况:允许编译器进行特定的优化,但事实并非如此.你问过"为什么不"?这是一个容易回答的问题:所有优化都没有实施,直到有人花费大量时间和精力:

  • 仔细设计优化:在什么条件下,优化触发而不是触发?优化应该有多普遍?你建议他们检测出类似的lambda体,但为什么要停在那里?您有两个相同的代码语句,那么为什么不为这些语句生成一次代码而不是两次?如果你有一重复的陈述怎么办?这里有大量的设计工作要做.
  • 特别地,该设计的一个重要方面是:用户是否可以"合理地"手动进行优化,同时仍保持代码可读.在这种情况下,是的,他们可以,轻松.只需将重复的lambda分配给变量,然后使用该变量.自动执行某项操作的用户可以轻松完成自己的优化并不是一个非常有趣或引人注目的优化.
  • 你的例子是微不足道的; 现实世界的代码不是.你提出的设计对相同的嵌套 lambda 做了什么?等等.
  • 您的优化是否会导致调试器中的代码行为"看起来很奇怪"?您可能已经注意到,在调试打开优化时编译的代码时,调试器似乎表现得很奇怪; 那是因为生成的代码和原始代码之间不再有清晰的映射.您的优化会使情况变得更糟吗?用户可以接受吗?调试器是否需要了解优化?如果是这样,您将不得不更改调试器.在这种情况下,可能不是,但这些是你必须提出并回答的问题.
  • 获得专家审查的设计; 这占用了他们的时间,并可能导致设计的变化
  • 估计优化的优缺点 - 优化通常会产生隐藏成本,比如我之前提到的内存泄漏.特别是,优化通常会排除可能更好的其他优化.
  • 估算此优化的全球总节约量.优化是否会影响实际代码?它是否会改变该代码的正确性?世界上任何地方都有任何生产代码会破坏这种优化并导致X公司的首席技术官要求微软的CTO要求修复吗?如果答案是肯定的,那么您可能想要不进行此优化.C#不是玩具.每天都有数以百万计的人依赖于正确的操作.
  • 编译时进行优化的估计负担是多少?在按键之间不必进行编译,但它必须非常快.在编译器中的公共代码路径中引入超线性算法的任何东西都是不可接受的.您可以实现优化,使其在代码大小上呈线性吗?请注意,我之前绘制的算法 - 比较所有对 - 在代码大小上是超线性的.(练习:对所有lambda对进行树比较的最坏情况是什么?)
  • 实际上实现优化.我鼓励你这样做.
  • 测试优化; 它实际上产生了更好的代码吗?什么指标?不会导致任何度量标准变化的优化不是优化.
  • 注册以永久修复优化中的错误.

您想要的优化根本不符合标准.没有人写这样的代码.如果他们这样做了,并且他们关心它复制了一个对象,他们可以轻松地自行修复它.因此,优化优化了不存在的代码,以获得"胜利",即在程序将分配的数百万个对象中构建单个对象.不值得.

但是,如果您认为是这样,请继续执行并提交拉取请求.请务必提交我上面提到的调查结果,因为这些是实际工作的地方.实施通常是花在功能上的总工作量的最小部分; 这就是为什么C#是一种成功的语言.