动态方法、表达式树和DLR

5

我有一些关于DynamicMethods、Expression Trees和DLR之间的交互和关系的问题。

  1. 我知道LambdaExpression.Compile在内部使用ILGenerator来创建委托。然而,编译后的LambdaExpression和DynamicMethod之间存在一些根本性的区别。例如:

    a. DynamicMethods调用速度更快

    b. 编译后的LambdaExpressions可以嵌入闭包(非基元值的ConstantExpressions)

    c. 编译后的LambdaExpressions没有DeclaringType。

    问题:

    a. 为什么DynamicMethods调用速度比编译后的LambdaExpressions更快?

    b. 编译后的LambdaExpressions有什么特殊之处,使其能够使用闭包?当我使用非ConstantExpression时,表达式树实际上是否生成了一个闭包类?如果是这样,生成的类放在哪里?

    c. 编译后的LambdaExpressions在运行时放在哪里?它们的支持是在哪里实现的?它不能仅仅是Reflection.Emit,对吗?

  2. 我知道动态关键字实际上只是一个编译器技巧,用于发出CSharp CallSites、Binders等。据我所知,在内部,它们生成Expression Trees,并使用裁剪版的C#编译器。

    问题:

    a. 生成的表达式树是一般CallSiteBinders的函数,还是Microsoft.CSharp dll中特定实现和用法的函数?

    b. 这些表达式树由DynamicExpression节点组成吗?还是其他什么东西?如果是其他东西,为什么?

    c. 裁剪版的C#编译器在哪里以及为什么起作用?它与常规调用LambdaExpression.Compile、DynamicMethods或任何类型的IL生成有什么不同?我可以理解CallSiteBinders如何用于构建Expression Trees,但为什么在转换发生后需要C#编译器?一旦它变成Expression Tree(只是一个API),C#与此有什么关系?


1
你为什么认为DynamicMethod更快?你的这个说法基于什么依据? - svick
1
另外,我认为你所提出的这两个问题并不是真正相关的。你应该将它们作为两个独立的问题来提出。 - svick
我个人的经验表明它们更快,而且正如在这里引用的https://dev59.com/GXM_5IYBdhLWcg3wn0vT(尽管标题表明相反),以及这里https://dev59.com/imTVa4cB1Zd3GeqP_QsY。然而,差异非常微小,我一直认为这只是发出的IL存在轻微差异所致。 - Jeff
我将它们作为一个问题提出,因为我想了解DLR与表达式树的编译和调用之间是否存在关系。原始的DLR(它是一个单独的dll)实际上只是表达式树的外部实现(包括Expression.Dynamic支持),与.NET 2.0兼容... - Jeff
我的意思是有些人(比如我)可能只知道你的一些问题的答案。如果你对问题1有一个很好的答案,对问题2有一个好的答案,对两个问题都有一个还可以的答案,那么SO模型就会崩溃。那么你该如何决定接受或点赞哪一个呢? - svick
2个回答

4
我对 dynamic 不是很了解,因此我只会回答你问题的第一部分。
“为什么DynamicMethods比编译后的LambdaExpressions更快?”如果真的是这样的话,我会非常惊讶,因为Expression.Compile() 内部使用了DynamicMethod
“编译后的LambdaExpressions有什么特别之处,可以支持闭包吗?当我使用非ConstantExpression时,Expression Tree是否真正生成了闭包类?如果是这样,生成的类放在哪里?”这很容易验证。只需要查看从表达式树编译生成的委托的TargetMethod。您会注意到Target(和Method的第一个参数)是System.Runtime.CompilerServices.Closure。这是一个包含字段object[] Constants的类,用于存储来自ConstantExpression 的非原始值。
“编译后的LambdaExpressions在运行时放置在哪里?它们的支持是如何实现的?它不能仅仅是Reflection.Emit,是吗?”就像我之前说的那样,Expression.Compile() 内部使用了DynamicMethod。所以,是的,它就是Reflection.Emit。

我现在看到了有关编译的 lambda 中闭包类的部分。这回答了我很多问题。谢谢。它也解释了为什么 JIT 优化对于常规 dynamicmethod 和由表达式树创建的方法可能会有非常不同的表现(因为它绑定到 Closure 类的实例)。 - Jeff
我反汇编了Closure类和更多的表达式树API,现在我明白了(所有第1点)。我想,弄清楚我的DLR问题最简单的方法是进行更多的反汇编。谢谢。我错过了关于闭包类如何跟踪常量的关键部分。 - Jeff

2
好的,我不能回答你所有的问题,但我可以回答其中一些,我认为这可能会回答你的大部分问题。至少它会给你足够的信息来继续研究。
动态方法比编译后的Lambda表达式更快的原因是什么?
我不认为它们更快,也许你测量有误,这可能是JIT编译的差异。
编译后的Lambda表达式有什么特殊之处,可以允许闭包吗?当我使用非ConstantExpression时,表达式树是否实际生成了一个闭包类?如果是这样,那么生成的类在哪里?
对于这个问题,我不确定。我假设Expression.Constant可以包含引用类型,那么这就不是问题,但如果它确实只能包含值类型,那么我猜编译器会生成一个表达式,在其中捕获闭包的变量只是作为参数传递。
编译后的Lambda表达式在运行时放在哪里?支持它们的实现在哪里实现?它不能只是Reflection.Emit吧?
System.Linq.Expressions实际上只是Reflection.Emit上面更友好的API,因此它们默认情况下与Reflection.Emit一样存储在内存中(尽管使用Reflection.Emit可以保存发出的代码)。
生成的表达式树是CallSiteBinders的函数,还是Microsoft.CSharp dll中它们的特定实现和用法?
我只做了一点System.Dynamic的工作,所以我无法回答这个问题,但我的理解是CallSiteBinder只是缓存和调用表达式,但将实际生成的内容传递给其他东西(即DynamicObject)。但是,你可能比我更了解这里。
这些表达式树由DynamicExpression节点组成吗?还是其他什么东西?如果是其他的,为什么?
不,动态仍然受到.NET中其他所有规则的约束。动态只是表示“在运行时,当我执行x时,去尝试构建我通常会编写的代码并为我执行它。”像DynamicObject这样的东西只会构建一个普通的表达式树,动态对象只是提供一些元数据,以便您可以实际构建该树(例如返回类型、访问类型、名称等)。
精简版的C#编译器在哪里发挥作用?为什么和LambdaExpression.Compile或DynamicMethods或任何类型的IL生成的常规调用不同?我可以理解CallSiteBinders如何用于构建表达式树,但为什么转换发生后需要C#编译器?一旦它变成表达式树(只是一个API),C#与其有什么关系?
我不确定您所说的精简编译器或运行时实际上对生成的IL代码进行了什么处理(我认为这就是您想要的),因此我不认为我能回答这个问题。

然而,就像我之前说的那样: System.Linq.Expression 只是 Reflection.Emit 上友好的 API。表达式树只是它需要的信息,以便前往 Reflection.Emit 生成动态方法并将其返回给您。


编译器参考资料的简化版本是基于 Eric Lippert 的评论。我在问题中应该注明,抱歉。 - Jeff

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