【问题标题】:C# speedup method call of a generic class using expressions使用表达式的泛型类的 C# 加速方法调用
【发布时间】:2016-09-09 10:52:16
【问题描述】:

我需要调用一个泛型类的实例方法。签名如下所示:

public class HandlerFactory
{
    public static IHandler<T> Create<T>();
}

public interface IHandler<T>
{
    T Read(Stream s);

    void Write(Stream s, T v);
}

我设法通过使用表达式和 DynamicInvoke 使其工作。遗憾的是 DynamicInvoke 的性能并不是那么好。我无法将委托转换为 Action&lt;MemoryStream, T&gt;,因为我在编译时不知道类型。

public class Test
{
    public static void Write(MemoryStream s, object value)
    {
        var del = GetWriteDelegateForType(value.GetType());

        // TODO: How to make this faster?
        del.DynamicInvoke(s, value);
    }

    private static object GetHandlerForType(Type type)
    {
        var expr = Expression.Call(typeof(HandlerFactory), "Create", new[] { type });
        var createInstanceLambda = Expression.Lambda<Func<object>>(expr).Compile();
        return createInstanceLambda();
    }

    private static Delegate GetWriteDelegateForType(Type type)
    {
        var handlerObj = GetHandlerForType(type);
        var methodInfo = handlerObj.GetType().GetMethod("Write", new[] { typeof(MemoryStream), type });

        var arg1 = Expression.Parameter(typeof(MemoryStream), "s");
        var arg2 = Expression.Parameter(type, "v");

        var handlerObjConstant = Expression.Constant(handlerObj);
        var methodCall = Expression.Call(handlerObjConstant, methodInfo, arg1, arg2);

        var lambda = Expression.Lambda(methodCall, arg1, arg2);

        return lambda.Compile();
    }
}

请注意,我没有对 lambda 生成进行基准测试,只是对 DynamicInvoke 的调用。

有什么办法可以用更快的东西代替 DynamicInvoke?

更新:我评估了包含代码示例的 3 个答案,并选择 Lasse V. Karlsen 答案,因为它很简单。 (注意 Grax 的代码:尽管缓存了 MakeGenericMethod 调用,但它似乎比将 Invoke 包装在委托中要慢得多)

             Method |        Median |     StdDev |
------------------- |-------------- |----------- |
           MyLambda | 1,133.2459 ns | 25.1972 ns |
       ExplicitCall |     0.6450 ns |  0.0256 ns |
 Test2DelegateLasse |    10.6032 ns |  0.2141 ns |
         LambdaGroo |    10.7274 ns |  0.1099 ns |
         InvokeGrax |   349.9428 ns | 14.6841 ns |

【问题讨论】:

  • 你能创建一个人们可以修改的minimal reproducible example吗?对一组特定的现有代码进行计时,这样我就可以在我的计算机上运行它以获得基线,然后对其进行修改以查看是否可以找到更快的方法?
  • 拥有一个插件工厂的通用接口并没有什么意义,恕我直言。
  • 这是一个第三方库,所以我无法确定它是否有意义。这是一个演示性能的完整工作示例:pastebin.com/K3q4dgMk。在我的 PC 上,将直接方法调用与 DynamicInvoke 进行比较需要 820 毫秒到 2 毫秒。
  • 我看不出将 Invoke 包装在委托中对您有何帮助。委托是特定于类型的。如果您已经知道类型,只需直接调用显式处理程序即可。否则你仍然需要一种从对象类型到显式类型调用的机制。

标签: c# .net generics lambda


【解决方案1】:

这样做的方法是通过一个适当的泛型方法,将一个从object 转换为T 的转换,并跳过整个动态调用。

根据您在 pastebin 中的代码,这是您的 Test 类的新版本:

public class Test2
{
    private static readonly Action<MemoryStream, object> del;

    static Test2()
    {
        var genericCreateMethod = typeof(Test2).GetMethod("CreateWriteDelegate", BindingFlags.Static | BindingFlags.NonPublic);
        var specificCreateMethod = genericCreateMethod.MakeGenericMethod(typeof(Model));
        del = (Action<MemoryStream, object>)specificCreateMethod.Invoke(null, null);
    }

    public static void Write(MemoryStream s, object value)
    {
        del(s, value);
    }

    private static Action<MemoryStream, object> CreateWriteDelegate<T>()
    {
        var handler = HandlerFactory.Create<T>();
        return delegate (MemoryStream s, object value)
        {
            handler.Write(s, (T)value);
        };
    }
}

在我的机器上,您的代码与上述一样执行为:

您的测试:1285 毫秒
我的测试:20ms
显式:4ms

【讨论】:

  • 您的解决方案是最简单的解决方案,它甚至比我的解决方案在我的机器上运行速度快约 5%。但是您可能希望将初始化从静态构造函数移动到 Write 方法中,因为现在您已经硬编码了 &lt;MemoryStream, Model&gt; 泛型参数。此外,委托语法是如此 2006。:)
  • 委托语法可能是旧的,但我倾向于在此处发布代码时谨慎行事,以避免人们因为错误的原因而忽略代码:) 代码绝不是生产就绪的,它应该被包装在一个类中,该类按类型缓存创建的委托并按需创建它们。我只是镜像了 OP 的 pastebin 代码,以便于比较。
  • 我的意思是,您将 value.GetType() 替换为 typeof(Model),这(至少对我而言)使得比较它变得有点困难。据我所知,OP 的代码中没有Model
  • 感谢您出色而简单的回答。我用适当的基准测试结果更新了我的帖子。
【解决方案2】:

编写一个泛型方法,并使用 MakeGenericMethod 和 Invoke 来调用它。

将您要调用的方法存储在静态变量中,这样 GetMethod 调用只需发生一次。

然后在该 MethodInfo 上调用 MakeGenericMethod 并在结果上调用。

private static MethodInfo GenericWriteMethod =
    typeof(Test).GetMethod("GenericWrite", BindingFlags.NonPublic | BindingFlags.Static);

public static void Write(MemoryStream s, object value)
{
    GenericWriteMethod
        .MakeGenericMethod(value.GetType())
        .Invoke(null, new object[] { s, value });
}

private static void GenericWrite<T>(MemoryStream s, T value)
{
    HandlerFactory.Create<T>().Write(s, value);
}

在我的测试中,这使其速度提高了 100 倍以上。

【讨论】:

  • 该解决方案也可以改进以避免MakeingGenericMethod。将此与一些表达式结合起来得到Action 将是最快的解决方案。
  • 我通过重写他粘贴在 pastebin 上的代码进行测试,以执行您在此处发布的相同操作,而用于显式写入的时间减少了大约 5 倍。这绝对是要走的路。
【解决方案3】:

您应该简单地创建一个Action&lt;Stream, object&gt;

static Action<Stream, object> GetWriteDelegateForType(Type type)
{
    // get the actual generic method
    var handlerObj = GetHandlerForType(type);
    var methodInfo = handlerObj
                .GetType()
                .GetMethod("Write", new[] { typeof(MemoryStream), type });

    // but use (Stream, object) parameters instead
    var streamArg = Expression.Parameter(typeof(Stream), "s");
    var objectArg = Expression.Parameter(typeof(object), "v");

    // this will cast object to T
    var tCast = Expression.Convert(objectArg, type);

    var handlerObjConstant = Expression.Constant(handlerObj);
    var body = Expression.Call(handlerObjConstant, methodInfo, streamArg, tCast);
    var lambda = Expression.Lambda<Action<Stream, object>>(body, streamArg, objectArg);

    // and compile to an actual Action<Stream, object>
    return lambda.Compile();
}

然后你就像一个普通的委托一样称呼它:

static void Write(MemoryStream s, object value)
{
    var action = GetWriteDelegateForType(value.GetType());
    action(s, value);
}

缓存委托也是个好主意:

static readonly ConcurrentDictionary<Type, Action<Stream, object>> _cache = 
   new ConcurrentDictionary<Type, Action<Stream, object>>();

static void Write(MemoryStream s, object value)
{
    var type = value.GetType();
    var action = _cache.GetOrAdd(type, GetWriteDelegateForType);
    action(s, value);
}

【讨论】:

  • 非常感谢您加快了我的表情代码。但我会坚持 Lasse V. Karlsen 的回答,因为我认为它比表达更容易理解。不过你的代码很快!
【解决方案4】:

您可以将其设为Action&lt;MemoryStream, object&gt; 并简单地使用Expression.Convert()v 的类型从object 更改为type

为了提高性能,您可以将这些 Actions 存储在一些 Type-keyed 字典中(并发?),但表达式需要对 Create 内部的处理程序进行一些更改。

【讨论】:

  • 我已经缓存了表达式,问题只是对 DynamicInvoke 的调用很慢。我无法更改 HandlerFactory 或 IHandler 的代码,因为它是第三方库。
  • 我的解决方案不涉及对库的任何更改。您只需要更改GetWriteDelegateForType,使其接受object 类型的参数v,然后对其执行转换。然后可以将此类表达式树编译为非泛型 Action 并在没有任何性能损失的情况下调用。然后,Action 可以被缓存,其性能应该类似于常规方法之一。如果有任何帮助,我可以为生成的Action 添加伪代码,您可以稍后尝试将其转换为表达式树。
【解决方案5】:

添加另一个答案,因为这个答案与我的第一个答案明显不同。

TLDR:添加非泛型接口,创建字典以缓存类型的处理程序,使用非泛型 Write 方法调用处理程序。


为 ExplicitHandler 添加一个非泛型接口,使其更容易在非泛型代码中进行交互。

public interface IHandler
{
    void Write(Stream s, object v);
}

在ExplicitHandler中实现非泛型Write方法强制转换为T并调用泛型Write

    void IHandler.Write(Stream s, object v)
    {
        Write(s, (T)v);
    }

在字典中缓存处理程序

public class Test
{
    static Dictionary<Type, IHandler> delegates = new Dictionary<Type, IHandler>();

    public static void Write(MemoryStream s, object value)
    {
        IHandler handler;

        var type = value.GetType();
        if (!delegates.TryGetValue(type, out handler))
        {
            handler = (IHandler)typeof(HandlerFactory).GetMethod(nameof(HandlerFactory.Create)).MakeGenericMethod(type).Invoke(null, null);
            delegates[type] = handler;
        }

        handler.Write(s, value);
    }
}

在此处重写您的源代码:http://pastebin.com/hmfj2Gv2

【讨论】:

  • 感谢您的回复,但正如 cmets 中所述,我无法修改 IHandler 接口,因为它来自第三方库。
  • 非泛型 IHandler 是与泛型 IHandler 接口完全不同的接口。你甚至可以称它为别的东西。真正的问题是您是否可以修改显式处理程序以实现您的非通用写入方法。如果没有,您始终可以编写一个包含非泛型 Write 方法和对泛型处理程序的引用的包装类。
猜你喜欢
  • 2021-02-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-12
  • 2012-01-11
  • 1970-01-01
相关资源
最近更新 更多