简体   繁体   中英

C# speedup method call of a generic class using expressions

I need to call an instance method of a generic class. The signature looks like this:

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

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

    void Write(Stream s, T v);
}

I managed to get it working by using expressions and DynamicInvoke. Sadly the performance of DynamicInvoke isn't that great. I can't cast the delegate to an Action<MemoryStream, T> because I don't know the type at compile time.

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();
    }
}

Please note, I didn't benchmark the lambda generation, just the call to DynamicInvoke.

Is there any way to replace DynamicInvoke with something faster?

Update: I evaluated the 3 answers which contained code samples and choose to go with Lasse V. Karlsen answer because of the simplicity. (Note on Grax's code: despite caching the MakeGenericMethod call it seems to be way slower than wrapping Invoke in a delegate)

             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 |

The way to do this is to go through a proper generic method, wrapping up a cast from object to the T , and skipping the entire dynamic invoke.

From your code in the pastebin, here's a new version of your Test class:

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);
        };
    }
}

On my machine your code, with the above as well executes as:

Your test: 1285ms
My test: 20ms
Explicit: 4ms

Write a generic method and use MakeGenericMethod with Invoke to call it.

Store the method you want to call in a static variable so the GetMethod call only needs to happen once.

Then call MakeGenericMethod on that MethodInfo and Invoke on the result.

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);
}

In my testing, this made it over 100 times faster.

You should simply create an Action<Stream, object> instead:

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();
}

And then you just call it like a plain delegate:

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

It would be also good idea to cache the delegate:

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);
}

You could just make it an Action<MemoryStream, object> and simply use Expression.Convert() to change type of v from object to type .

For performance gain, you could just store those Action s in some Type -keyed dictionary (concurrent?), but the expressions would require some changes to Create the the handlers inside of them.

Adding another answer because this one is significantly different than my first one.

TLDR: Add a non-generic interface, create a dictionary to cache the handler for a type, call the handler using a non-generic Write method.


Add a non-generic interface to ExplicitHandler to make it easier to interact with in non-generic code.

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

Implement the non-generic Write method in ExplicitHandler to cast to T and call the generic Write

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

Cache the handlers in a dictionary

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);
    }
}

Rewrite of your source here: http://pastebin.com/hmfj2Gv2

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM