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

Weird behaviour of c# compiler due caching delegate

假设我有以下程序:

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

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

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

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

都一样写成

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

?好吧,让我从几个方面回答这个问题。

首先,编译器允许进行优化吗?是的。该规范指出,C# 编译器 允许 将两个执行完全相同操作的 lambda 表达式放入一个委托中。事实上,您可以看到它已经部分地进行了这种优化:它创建每个委托 一次 并将其保存起来,这样它就不必稍后在代码运行时再次创建它又叫了。请注意,在仅调用一次代码的情况下,这是一种内存浪费。

其次,是否需要编译器进行缓存优化?不可以。规范指出编译器只允许进行优化,而不是要求

是否需要编译器来进行您想要的优化?显然不是,因为它没有。 允许,也许未来版本的编译器会。编译器是open-source;如果你关心这个优化,去写它并提交一个拉取请求。

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

所以现在我们有一种情况:编译器允许进行特定的优化,但它没有。你问过"why not"?这是一个很容易回答的问题:只有有人花费大量时间和精力来实现所有优化:

  • 精心设计优化:究竟在什么条件下触发和不触发优化?优化应该有多普遍?你曾建议他们检测到类似的 lambda 体,但为什么要停在那里?您有两个相同的 statements 代码,那么为什么不为这些 statements 生成一次代码而不是两次呢?如果您有重复的 组语句 怎么办?这里有大量的设计工作要做。
  • 特别是,设计的一个重要方面是:用户能否在保持代码可读性的同时合理地进行优化"by hand"。在这种情况下,是的,他们可以,很容易。只需将重复的 lambda 分配给一个变量,然后使用该变量。自动执行一些用户本来可以轻松完成的优化并不是真正有趣或令人信服的优化。
  • 你的例子很简单; real-world代码不是。您提出的设计如何使用相同的 nested lambda?等等。
  • 您的优化是否导致调试器中的代码行为 "look weird"?您可能已经注意到,在调试启用了优化的情况下编译的代码时,调试器的行为似乎很奇怪;那是因为生成的代码和原始代码之间不再有明确的映射。您的优化会使情况变得更糟吗?用户能接受吗?调试器是否需要了解优化?如果是这样,您将不得不更改调试器。在这种情况下,可能不会,但这些是您必须提出和回答的问题。
  • 获得专家评审的设计;这会占用他们的时间,并且可能会导致设计发生变化
  • 估计优化的利弊——优化通常有隐性成本,比如我之前提到的内存泄漏。特别是,优化通常 排除 其他可能更好的优化。
  • 估计此优化的总节省 world-wide。优化真的会影响 real-world 代码吗?它会改变该代码的正确性吗?世界上任何地方是否有任何生产代码会破坏此优化并导致 X 公司的 CTO 致电 Microsoft 的 CTO 要求修复?如果答案是肯定的,那么您可能不想进行此优化。 C# 不是玩具。每天都有数以百万计的人依赖于它的正确运行。
  • 编译时进行优化的估计负担是多少?编译不必在击键之间发生,但它必须非常快。任何在编译器的公共代码路径中引入超线性算法的东西都是不可接受的。您能否实施优化以使其代码大小呈线性?请注意,我之前草拟的算法——比较所有对——在代码大小上是超线性的。 (练习:做 a 的最坏情况的渐近性能是什么?所有 lambda 对的树比较?)
  • 实际执行优化。我鼓励你这样做。
  • 测试优化;它真的能产生更好的代码吗?在什么指标上?不改变任何指标的优化不是优化。
  • 注册以永久修复优化中的错误。

你想要的优化根本达不到标准。没有人会这样写代码。如果他们这样做了,并且他们关心它复制了一个对象,他们可以很容易地自己修复它。因此,优化会优化不存在的代码,以便获得一个 "win",它是程序将分配的数百万个对象中的单个对象的构造。不值得。

但同样,如果您认为是,请继续实施并提交拉取请求。确保提交我上面提到的调查结果,因为这些才是真正的工作所在。实现通常是花在某个功能上的总工作量的最小部分;这就是为什么 C# 是一种成功的语言。