【问题标题】:Obscure compiler's lambda expression translation晦涩编译器的 lambda 表达式翻译
【发布时间】:2014-03-21 16:58:13
【问题描述】:

我研究了 Y Combinator(使用 c# 5.0),当这个方法时我感到很惊讶:

public static  Func<T1, Func<T2, TOut>> Curry<T1, T2, TOut> ( this Func<T1, T2, TOut> f)
{
    return a => b => f(a, b);
}

...被编译器翻译成这样:

public static Func<T1, Func<T2, TOut>> Curry<T1, T2, TOut>(this Func<T1, T2, TOut> f)
        {
            first<T1, T2, TOut> local = new first<T1, T2, TOut>();
            local.function = f;
            return new Func<T1, Func<T2, TOut>>(local.Curry);
        }
    private sealed class first<T1, T2, TOut>
    {
        private sealed class second
        {
            public first<T1, T2, TOut> ancestor;
            public T1 firstParameter;
            public TOut Curry(T2 secondParameter)
            {
                return ancestor.function(firstParameter, secondParameter);
            }
        }
        public Func<T1, T2, TOut> function;
        public Func<T2, TOut> Curry(T1 firstParameter)
        {
            second local = new second();
            local.ancestor = this;
            local.firstParameter = firstParameter;
            return new Func<T2, TOut>(local.Curry);
        }
    }

因此,当我们使用引用 second.Curry 的委托时,第二类是嵌套的,第一类不可用于垃圾收集。同时,我们在头等舱中所需要的只是功能。也许我们可以将它(委托)复制到第二类,然后可以收集第一类?是的,我们也应该做二等非嵌套,但似乎没关系。据我所知,代表是“按值”复制的,所以我可以建议它很慢,但同时我们复制 firstParameter?!那么,可能有人可以解释一下,为什么编译器会做所有这些事情?) 我谈论这样的事情:

private sealed class first<T1, T2, TOut>
{
        public Func<T1, T2, TOut> function;
        public Func<T2, TOut> Curry(T1 firstParameter)
        {
            second<T1, T2, TOut> local = new second<T1, T2, TOut>();
            local.function = function;
            local.firstParameter = firstParameter;
            return new Func<T2, TOut>(local.Curry);
        }
    }
    public sealed class second<T1, T2, TOut>
    {
        public T1 firstParameter;
        public Func<T1, T2, TOut> function;
        public TOut Curry(T2 secondParameter)
        {
            return function(firstParameter, secondParameter);
        }
    } 

【问题讨论】:

  • 你需要的不仅仅是函数,你正在关闭参数a,所以你需要存储它。
  • @SergeyBerezovskiy:Currying 允许您将两个参数的函数转换为一个参数的函数,该函数返回一个参数的函数。例如,假设您有一个函数add,即(a,b)=&gt;a+b。通过柯里化你有一个函数makeAdder,它接受一个数字并返回一个将这个数字加到一个数字上的函数。
  • @SergeyBerezovskiy 看起来像 en.wikipedia.org/wiki/Currying 的标准实现代码可能看起来像 var add5 = Add.Curry(5)
  • Alexei,第二个类有祖先字段,所以我们的第一个类在第二个存在和第二个存在之前不能被收集,因为我们的代表在 second.Curry 上有引用
  • @AlexeiLevenkov:我认为 OP 指出second 只使用ancestor 来获取function,那么为什么不让second 坚持function 而不是坚持转到ancestor?他的想法是,ancestor 可以更早收集。

标签: c# performance compiler-construction lambda


【解决方案1】:

这个问题很难理解。让我澄清一下。您的建议是编译器可以改为生成

public static Func<T1, Func<T2, TOut>> Curry<T1, T2, TOut>(this Func<T1, T2, TOut> f)
{
    first<T1, T2, TOut> local = new first<T1, T2, TOut>();
    local.function = f;
    return new Func<T1, Func<T2, TOut>>(local.Curry);
}
private sealed class first<T1, T2, TOut>
{
    private sealed class second
    {
        //public first<T1, T2, TOut> ancestor;
        public Func<T1, T2, TOut> function;
        public T1 firstParameter;
        public TOut Curry(T2 secondParameter)
        {
            return /*ancestor.*/function(firstParameter, secondParameter);
        }
    }
    // public Func<T1, T2, TOut> function;
    public Func<T2, TOut> Curry(T1 firstParameter)
    {
        second local = new second();
        // local.ancestor = this;
        local.function = function;
        local.firstParameter = firstParameter;
        return new Func<T2, TOut>(local.Curry);
    }
}

是吗?

您的说法是,由于这种情况,这是一种改进。

Func<int, int, int> adder = (x, y)=>x+y;
Func<int, Func<int, int>> makeAdder = adder.Curry();
Func<int, int> addFive = makeAdder(5);
  • addFivesecond 实例上的方法的委托
  • makeAdderfirst 实例上的方法的委托
  • 在原始代码生成中,second 保留ancestor,这是first 的同一个实例

所以如果我们说

makeAdder = null;

那么first的实例无法被收集。该实例无法再通过makeAdder 访问,但可以通过addFive 访问。

在提议的代码生成中,first 可以在这种情况下收集,因为无法通过addFive 访问该实例。

您是正确的,在这种特定情况下优化是合法的。然而,由于 Ben Voigt 在他的回答中描述的原因,它一般是不合法的。 如果fCurry 中发生变异,那么local.function 必须发生变异。但是local 无法访问second 的实例直到执行外部委托强>.

C# 编译器团队可以选择执行您已确定的优化,但到目前为止,节省的小笔钱根本不值得费心。

我们正在考虑让 Roslyn 按照您描述的方式进行优化;也就是说,如果外部变量已知不会发生变异,则更积极地捕获其值。我不知道这种优化是否适用于 Roslyn。

【讨论】:

  • 改进的引用局部性可能比内存占用更重要。在任何非玩具项目中仍然可以忽略不计。
  • 我想知道是否有任何代码使用反射来到达编译器生成的闭包类型。显然这样是不明智的。但是序列化框架会自动做这样的事情吗?
  • @Eric Lippert:非常感谢。它变得更加清晰。
【解决方案2】:

您已经使用了两次 lambda 运算符,因此您将获得两个匿名委托,并将捕获的变量提升为状态类型。

内部 lambda 的状态类型通过引用保持外部 lambda 的状态类型的原因是,这就是 C# 中捕获的工作方式:您捕获的是一个变量,而不是它的值。

其他一些语言(例如 C++11 lambdas)具有替代语法来指示按值捕获与按引用捕获。 C# 没有,并且只是通过引用捕获所有内容。支持按值语义并不是一个很好的理由,因为垃圾回收避免了 C++11 中没有按值捕获模式时会存在的生命周期问题。


C# 编译器是否会注意到该变量从未被写入,因此按值捕获与按引用捕获没有区别?可能,但这是编译器中的额外逻辑,对设计和代码的额外审查,额外的测试。所有这些都是为了在内存占用和局部性方面进行微小的、几乎微不足道的改进。它显然不符合成本效益标准,您可以在 Eric Lippert 的许多博客文章中找到讨论。

【讨论】:

  • 另外,JIT 编译器在执行时可能会进一步简化。
  • @Grzenio:我不认为合并类型是 JIT 允许做的优化之一。无论 C# 编译器如何处理代码的高级视图,这对于 JIT 来说更糟糕。请记住,两者都没有完整的程序视图。这与跨模块内联相反,因为它不仅需要知道对象使用的程序集,还需要知道所有用户。是什么阻止了另一段代码使用反射来改变ancestor.function
  • C# 编译器可以随意破坏以这种方式使用反射的程序,因为这会干扰 C# 实现细节。但是 JIT 不能,因为它不拥有这些细节,而 C# 编译器拥有。
  • 我们正在考虑您提到的优化——注意到在创建委托并按值捕获它之后外部变量永远不会发生变化——对于 Roslyn 但我不知道它是否已经实现.
  • 是的,我不认为只有在这种特殊情况下才能实现,非常感谢您的明确解释。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-01-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多