C#编译器由于缓存委托而导致的奇怪行为

9
假设我有以下程序:
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,当然。我同意你的观点。但是实现方式似乎有些奇怪。 - Alex Aparin
否则我不确定你想要什么,因为这段代码无法编译,所以你可能缺少一些部分。 - becike
@becike,我认为编译后的代码版本不会包含b1方法 - 这是主要问题。 - Alex Aparin
1个回答

18

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

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分配给一个变量,然后使用该变量。自动执行用户可以轻松完成的优化并不是一种非常有趣或引人注目的优化。
  • 你的例子是琐碎的;现实世界中的代码并不是这样的。你的提议对于相同的嵌套lambda做了什么?等等。
  • 你的优化是否会使调试器中的代码行为“看起来奇怪”?你可能已经注意到,当调试使用了启用优化的代码时,调试器似乎会表现出奇怪的行为;这是因为生成的代码和原始代码之间不再有清晰的映射。你的优化是否会使情况更糟?用户是否接受?调试器是否需要意识到优化?如果是这样,则必须更改调试器。在这种情况下,可能不需要,但这些是您必须询问和回答的问题。
  • 请专家审核设计;这需要占用他们的时间,并可能导致设计更改
  • 对优化的利弊进行估计-优化往往有隐藏的成本,如我之前提到的内存泄漏。特别是,优化通常排除

1
很棒的答案!我完全同意。 - Alex Aparin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接