【问题标题】:how to get an attribute getter as a Func<object,object>?如何将属性获取器作为 Func<object,object>?
【发布时间】:2017-10-16 03:45:35
【问题描述】:

简介 我正在编写一些严重依赖反射的代码。一些处于核心的部分被重复使用。以最轻微的方式对此进行优化都会给我带来很多(不同程度的)性能优势。

上下文 核心包括从对象中获取数据。目前我使用反射获取一个PropertyInfo对象,然后使用如下代码构造一个Func

    public static Func<object, object> BuildUntypedGetter(PropertyInfo propertyInfo)
    {
    var targetType = propertyInfo.DeclaringType;
    var methodInfo = propertyInfo.GetGetMethod();
    var exTarget = System.Linq.Expressions.Expression.Parameter(typeof(object), "t");
    var exBody0 = System.Linq.Expressions.Expression.Convert(exTarget, targetType);
    var exBody = System.Linq.Expressions.Expression.Call(exBody0, methodInfo);
    var exBody2 = System.Linq.Expressions.Expression.Convert(exBody, typeof(object));

    var lambda = System.Linq.Expressions.Expression.Lambda<Func<object, object>>(exBody2, exTarget);

    var action = lambda.Compile();
    return action;
    }

这个结果 Func 是我然后缓存和使用的。在其他对象类型上不调用它的实际类型和安全性是我不必担心的,因为这已经处理好了。

在上面的代码中,创建的 lambda 如下所示:

.Lambda #Lambda1<System.Func`2[System.Object,System.Object]>(System.Object $t) {
    (System.Object).Call ((MyObjectType)$t).get_Id()
}

其中 Id 是生成它的属性。

如您所见,这只是一个重定向。它所做的只是调用 getter。

问题 有没有办法只返回 getter (get_Id) 作为 Func&lt;object,object&gt; 而无需额外的演员和调用?或者是否有其他更有效的方法来调用对象类型作为实例类型的 object.attribute getter?

【问题讨论】:

  • 属性返回的是引用类型还是值类型?还是两者兼而有之?
  • 值和引用类型都需要支持。不过我想我们可以提前从元数据中获取这些信息,然后在需要时生成差异化代码(例如,if (valuetype) { create value lambda } else { create reference lambda }。
  • 嗯,有 Delegate.CreateDelegate 方法。不确定它是否允许投射对象。
  • 我是一般人,不,这是不可能的。它要求输入类型T 是协变的,这是不合法的。此外,输出类型TReturn 仅对引用类型是协变的; Func&lt;,object&gt; 不能从 Func&lt;,int&gt; 或其他原始特殊特化中分配。
  • @Wazner 我已经检查过该方法。但是,据我了解(可能是错误的),委托被绑定到非静态方法的特定 instance 。我需要能够在不创建新委托的情况下调用多个对象的 getter

标签: c# optimization lambda


【解决方案1】:

有没有办法只返回 getter (get_Id) 作为 Func 没有额外的演员和调用?

C# 不允许您在没有强制转换的情况下调用 getter(当然 dynamic 类型除外,但这是另一回事),因为这会违反 C# 所保证的类型安全。

但是您可以发出不包含此类转换并直接调用 getter 的 IL 代码。当然,当您像这样比编译器更聪明时,您有责任仅在正确类型的对象上调用此方法,否则您的应用程序可能会崩溃。

以下代码显示了如何发出此类 IL 代码。我的测量表明,在 Release 模式下,发出的委托约为。比您编译的 lambda 快 3 倍(在 Debug 模式下,它实际上似乎有点慢)。

public static Func<object, object> EmitUntypedGetter(PropertyInfo pi)
{
    DynamicMethod method = new DynamicMethod(
        "PropertyGetter",
        typeof(Object),
        new[] { typeof(Object) },
        Assembly.GetExecutingAssembly().ManifestModule);

    ILGenerator il = method.GetILGenerator(100);

    // Load object onto the stack.
    il.Emit(OpCodes.Ldarg_0);

    // Call property getter
    il.EmitCall(OpCodes.Callvirt, pi.GetGetMethod(), null);

    // If property returns value-type, value must be boxed
    if(pi.PropertyType.IsValueType)
        il.Emit(OpCodes.Box, pi.PropertyType);

    // Exit method
    il.Emit(OpCodes.Ret);

    return (Func<Object, Object>)method.CreateDelegate(typeof(Func<Object, Object>));
}


编辑:

在我的计算机上,发出代码的性能始终比 lambda 版本快得多。

在 Windows 10 Home 上测试,CPU Intel Core2 Q9400,使用 Visual Studio 2017 CE v. 15.4.0 编译,面向 .NET Framework 4.7 的控制台应用程序,发布模式(优化代码项目属性中的选项启用),在 Visual Studio 之外执行(附加了 VS,一些优化被禁用)

我的结果:

Compiled lambda (value type)     : 40827 ms
Compiled lambda (reference type) : 37558 ms
Emit (value type)                : 16963 ms
Emit (reference type)            : 11903 ms

用于测试的程序:

public struct MyClass
{
    public int I => 42;
    public string S => "foo";
}

public static void Main()
{
    var valueTypeProperty = typeof(MyClass).GetProperty("I");
    var referenceTypeProperty = typeof(MyClass).GetProperty("S");

    var lambdaValueTypeGetterDelegate = BuildUntypedGetter(valueTypeProperty);
    var lambdaReferenceTypeGetterDelegate = BuildUntypedGetter(referenceTypeProperty);
    var emitValueTypeGetterDelegate = EmitUntypedGetter(valueTypeProperty);
    var emitReferenceTypeGetterDelegate = EmitUntypedGetter(referenceTypeProperty);

    //warm-up - ensures that delegates are properly jitted
    lambdaValueTypeGetterDelegate(new MyClass());
    lambdaReferenceTypeGetterDelegate(new MyClass());
    emitValueTypeGetterDelegate(new MyClass());
    emitReferenceTypeGetterDelegate(new MyClass());

    TestDelegate("Compiled lambda (value type)     ", lambdaValueTypeGetterDelegate);
    TestDelegate("Compiled lambda (reference type) ", lambdaReferenceTypeGetterDelegate);
    TestDelegate("Emit (value type)                ", emitValueTypeGetterDelegate);
    TestDelegate("Emit (reference type)            ", emitReferenceTypeGetterDelegate);

    Console.ReadLine();
}

private static void TestDelegate(string description, Func<object, object> getterDelegate)
{
    const long LOOPS_COUNT = 1_000_000_000;
    var obj = new MyClass();

    Stopwatch sw = new Stopwatch();
    sw.Start();

    for (long i = 0; i < LOOPS_COUNT; i++)
    {
        getterDelegate(obj);
    }

    sw.Stop();

    Console.WriteLine($"{description}: {sw.ElapsedMilliseconds} ms");
}

【讨论】:

  • 您对此的看法很有趣(我自己没有使用过 IL 发射器)。但是,当我调用返回的方法时,它并不比我之前发布的 BuildUntypedGetter 快。返回的函数对于引用类型的 10*1000*1000*1000 调用都需要 43 秒,对于值类型大约需要 80 秒。最后还是间接调用。
  • 根据BenchmarkDotNet,ILGen 和Linq 版本的结果在8.4-8.6 ns/op 或多或少相同,错误为.2 ns。我会坚持最容易理解的(Linq)。根据 OP 对检索到的值所做的处理,最高效的解决方案可能是为 that 生成专门的代码。
  • @MikeStrobel:在我的电脑上,发出的委托要快得多。我包括用作基准的程序和我的结果。您能否确认您在 Visual Studio 之外进行了测试,因为附加的调试器会使发出的版本变慢?
  • Nuf 有趣的是,您的测试代码看起来和我的 90% 相同。因此,我查看了差异,即您的示例中的 MyClass。我在你的代码中改变了一件小事:你的 MyClass 结构。我将其更改为: public class MyClass { public string S { get;放; } 公共 int 我 { 得到;放; } }。通过此更改,结果是可比较的。在我的系统上,发射/参考稍快,而发射/值较慢。有趣的是,改变 S 和 I (所以首先是 int,然后是 string.. 会稍微改变时间。
  • @Floris:在将 MyClassstruct 更改为 class 后,我得到了以下结果:26514 ms, 24226 ms, 10114 ms, 4735 ms,因此虽然对于值类型,emit 仍然快近 3 倍,但它是现在引用类型的速度提高了 5 倍(当 MyClass 属性定义更改为 {get; set;} 时只有 4 倍)。我注意到当 benchark 运行 64 位进程(prefer 32-bit 选项禁用)时,emit 版本没有那么快,但仍然比 lambda 版本快得多。我还尝试了另外两台计算机,结果相似。
【解决方案2】:

有没有办法将 getter (get_Id) 作为 Func 返回,而不需要额外的强制转换和调用?

无法满足您的要求。虽然通常可以将委托直接动态绑定到方法(或属性访问器),但委托类型必须与您要绑定的方法签名兼容。对你来说不是这样。

考虑这个例子:

class MyClass {
    string Id { get; set; }
}

在这里,用Func&lt;,&gt; 表示的访问器将是Func&lt;MyClass, String&gt;。但是,您需要Func&lt;object, object&gt;。您正在更改参数类型和返回类型。这仅在某些限制条件下是允许的。

类型Func&lt;-T, +TReturn&gt; 在其输入类型T 上是逆变,在其输出类型TReturn 上是协变。因此,当T 的目标类型更具体TReturn 的目标类型不太具体时,Func&lt;,&gt; 实例可以转换为另一个实例化。例如:

Func&lt;object, *&gt; 可以转换为Func&lt;string, *&gt;,因为接收object 的函数总是可以接收string。反之则不然。

Func&lt;*, string&gt; 可以转换为Func&lt;*, object&gt;,因为返回string 的函数总是返回object。同样,反之亦然。

在 C# 中,这些规则是严格执行的。当泛型参数仅作为输出出现时,它可以是协变的;或者当它仅作为输入出现时是逆变的。此外,这种差异仅适用于引用类型,这意味着Func&lt;*, int&gt; 不能分配给Func&lt;*, object&gt;

恐怕没有办法解决:绑定到Func&lt;object, object&gt; 的任何方法都必须接受object 参数,这意味着您需要一个转换为源类型的中间方法。它还需要显式地装箱原始返回值。虽然您可以通过直接发出 IL 来消除一些转换,正如 @Ňuf 所展示的那样,但我认为您不会看到有意义的影响。

但是,根据您如何使用提取的值,可能会有更好的方法。与其使用生成的代码将值拉入您可以对其进行操作的 C# 世界,不如采取相反的做法:将该逻辑下推到生成的代码中,您可以根据您正在使用的类型对其进行专门化处理。或者,或者,使该逻辑通用,并使用运行时代码生成来分派到正确的通用实例。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-05-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多