【问题标题】:Weird behaviour of c# compiler due caching delegate由于缓存委托,c#编译器的奇怪行为
【发布时间】:2017-06-14 19:40:19
【问题描述】:

假设我有以下程序:

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));也有 1
  • @becike,是的,当然。我同意你的看法。但实现似乎很奇怪
  • 否则我不确定你想要什么,因为这段代码没有编译,所以你错过了几部分
  • @becike,我认为编译后的代码版本不会包含 b1 方法 - 这是主要问题

标签: c# clr compiler-optimization


【解决方案1】:

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

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 生成单个委托。事实上,您可以看到它已经部分地进行了这种优化:它创建每个委托一次并将其保存起来,以便以后再次调用代码时不必再次创建它。请注意,在只调用一次代码的情况下,这会浪费内存。

其次,是否需要编译器进行缓存优化?不。规范要求编译器仅允许进行优化,而不是必需

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

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

所以现在我们遇到了一种情况:编译器被允许进行特定优化,但它没有。你问过“为什么不”?这是一个很容易回答的问题:直到有人花费大量时间和精力来完成所有优化:

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

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

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

【讨论】:

  • 很好的答案!我完全同意。
猜你喜欢
  • 2013-09-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-28
  • 2021-03-08
  • 1970-01-01
相关资源
最近更新 更多