mardi 21 janvier 2020

How to create a fast-call delegate that has parameters and return type of private types, speeding up DynamicInvoke

I'm struggling with creating a call to the private ImmutableDictionary.Add, which allows me to utilize the KeyCollisionBehavior for finer control (the Add method only throws when key and value are different, I need it to throw always).

I can get where I want to be with basic reflection, however, the overhead of calling Invoke on MethodInfo, or DynamicInvoke on the delegate is significant (in fact, it almost triples the time on each call, which is too significant in my scenario).

The signature of the functions I need to call:

/// call property Origin, this returns a private MutationInput<,>
private MutationInput<TKey, TValue> Origin {get; }

/// call ImmutableDictionary<,>.Add, this takes MutationInput<,> and returns private MutationResult<,>
private static MutationResult<TKey, TValue> Add(TKey key, TValue value, KeyCollisionBehavior<TKey, TValue> behavior, MutationInput<TKey, TValue> origin);

/// then call MutationResult<,>.Finalize
internal ImmutableDictionary<TKey, TValue> Finalize(ImmutableDictionary<TKey, TValue> priorMap);

The challenge here being that I need to pass a private type around, and that the private type is part of the signature.

Normally, after calling CreateDelegate, you can simply cast it to a Func<X, Y, Z> and this gives near-direct calling speed. But I don't know how to create a Func<,> if the generic types are private and/or not known at compile time. Using object doesn't work, gives a runtime exception on the cast.

Here's a shortened version (removed a lot of try/catch and checks) of the code I currently have. This works:

/// Copy of enum type from Github source of ImmutableDictionary
type KeyCollisionBehavior =
    /// Sets the value for the given key, even if that overwrites an existing value.
    | SetValue = 0
    /// Skips the mutating operation if a key conflict is detected.
    | Skip = 1
    /// Throw an exception if the key already exists with a different value.
    | ThrowIfValueDifferent = 2
    /// Throw an exception if the key already exists regardless of its value.
    | ThrowAlways = 3

/// Simple wrapper DU to add type safety
type MutationInputWrapper = 
    /// Wraps the type ImmutableDictionary<K, V>.MutationInput, required as 4th param in the internal Add#4 method
    | MutationInput of obj

/// Simple wrapper to add type-safety
type MutationResultWrapper =
    /// Wraps the type ImmutableDictionary<K, V>.MutationResult, which is the result of an internal Add#4 operation
    | MutationResult of obj

/// Type abbreviation
type BclImmDict<'Key, 'Value> = System.Collections.Immutable.ImmutableDictionary<'Key, 'Value>

/// Private extensions to ImmutableDictionary
type ImmutableDictionary<'Key, 'Value>() =
    static let dicType = typeof<System.Collections.Immutable.ImmutableDictionary<'Key, 'Value>>
    static let addMethod = dicType.GetMethod("Add", BindingFlags.NonPublic ||| BindingFlags.Static)
    static let addMethodDelegate = 
        let parameters = addMethod.GetParameters() |> Array.map (fun p -> p.ParameterType)
        let funType = 
            typedefof<Func<_, _, _, _, _>>.MakeGenericType [|
                parameters.[0]
                parameters.[1]
                parameters.[2]
                parameters.[3]
                addMethod.ReturnType
            |]
        Delegate.CreateDelegate(funType, addMethod) // here one would normally cast to Func<X, Y...>

    static let mutationResultFinalizeMethod = 
        if not(isNull addMethod) && not(isNull(addMethod.ReturnParameter)) then
            /// Nested private type MutationResult, for simplicity taken from the return-param type of ImmutableDictionary.Add#4
            let mutationResultType = addMethod.ReturnParameter.ParameterType
            if not(isNull mutationResultType) then
                mutationResultType.GetMethod("Finalize", BindingFlags.NonPublic ||| BindingFlags.Instance ||| BindingFlags.DeclaredOnly)
            else
                null
        else
            null

    /// System.Collections.Immutable.ImmutableDictionary.get_Origin  // of valuetype ImmutableDictionary<,>.MutationInput<,>
    static let getOrigin = dicType.GetProperty("Origin", BindingFlags.NonPublic ||| BindingFlags.Instance)

    /// Calls private member ImmutableDictionary<,>.Add(key, value, behavior, origin), through reflection
    static member private refl_Add(key: 'Key, value: 'Value, behavior: KeyCollisionBehavior, MutationInput origin) =
        // use Invoke or DynamicInvoke makes little difference.
        //addMethod.Invoke(null, [|key; value; behavior; origin|])
        addMethodDelegate.DynamicInvoke([|box key; box value; box <| int behavior; origin|])
        |> MutationResult

    /// Gets the "origin" of an ImmutableDictionary, by calling the private property get_Origin
    static member private refl_GetOrigin(this: BclImmDict<'Key, 'Value>) =
        getOrigin.GetValue this
        |> MutationInput

    /// Finalizes the result by taking the (internal) MutationResult and returning a new non-mutational dictionary
    static member private refl_Finalize(MutationResult mutationResult, map: BclImmDict<'Key, 'Value>) =
        mutationResultFinalizeMethod.Invoke(mutationResult, [|map|])
        :?> BclImmDict<'Key, 'Value>

    /// Actual Add, with added control through CollisionBehavior
    static member InternalAddAndFinalize(key: 'Key, value: 'Value, behavior, thisMap) =
        let origin = ImmutableDictionary.refl_GetOrigin(thisMap)
        let mutationResult = ImmutableDictionary.refl_Add(key, value, behavior, origin)
        let finalizedMap = ImmutableDictionary.refl_Finalize(mutationResult, thisMap)
        finalizedMap

I realize the code above is in F#, but if you know how to fix this in C# I've no problem with translating your answer into my preferred target language.





Aucun commentaire:

Enregistrer un commentaire