Как создать делегат быстрого вызова, который имеет параметры и тип возвращаемых личных типов, ускоряя DynamicInvoke - PullRequest
2 голосов
/ 21 января 2020

Я борюсь с созданием вызова частного ImmutableDictionary.Add, который позволяет мне использовать KeyCollisionBehavior для более точного управления (метод Add выбрасывает только тогда, когда ключ и значение отличаются, мне нужно его бросить всегда).

Я могу получить то, что хочу, с отражением basi c, однако издержки вызова Invoke для MethodInfo или DynamicInvoke для делегата значительны (на самом деле , это почти в три раза увеличивает время каждого вызова, что слишком важно для моего сценария).

Подпись функций, которые мне нужно вызвать:

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

Сложность заключается в том, что я необходимо передать закрытый тип, и этот закрытый тип является частью подписи.

Обычно, после вызова CreateDelegate, вы можете просто привести его к Func<X, Y, Z>, и это дает почти прямой вызов скорость. Но я не знаю, как создать Func<,>, если типы generi c являются частными и / или не известны во время компиляции. Использование object не работает, выдает исключение времени выполнения при приведении.

Вот сокращенная версия (удалено много попыток / уловов и проверок) кода, который у меня есть на данный момент. Это работает:

/// 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

Я понимаю, что приведенный выше код написан на F #, но если вы знаете, как это исправить в C# У меня нет проблем с переводом вашего ответа на мой предпочитаемый целевой язык.

Ответы [ 3 ]

2 голосов
/ 22 января 2020

Я думаю, что другие комментаторы считают, что эта стратегия может быть не самой лучшей в данном конкретном случае. Однако здесь есть общая проблема, которую я буду принимать за чистую монету: как создать делегата для метода, который имеет типы, к которым вы не можете обращаться по имени, потому что они являются частными или внутренними?

Поскольку вы нельзя ссылаться на некоторые имена типов, вы не можете создать строго типизированный делегат, который был бы намного быстрее, чем Invoke / DynamicInvoke. В этом случае идея состоит в том, чтобы сгенерировать IL для метода-оболочки во время выполнения, используя System.Reflection.Emit.DynamicMethod, который вызывает методы с недоступными типами, но эта обертка предоставляет только те типы, к которым у вас есть доступ. DynamicMethod может «принадлежать» типу в другой сборке, минуя некоторые проверки видимости. Сложность состоит в том, что вы должны точно указать среде выполнения, какой IL будет выдаваться для этого метода-оболочки, поэтому в нем может быть сложно реализовать сложную логику c. В этом случае все достаточно просто написать вручную: получить свойство (Origin) и вызвать два метода (Add и Finalize).

Вот реализация в C# для этого:

enum KeyCollisionBehavior
{
    SetValue = 0,
    Skip = 1,
    ThrowIfValueDifferent = 2,
    ThrowAlways = 3,
}

internal static class ImmutableDictionaryHelper<TKey, TValue>
{
    private static readonly MethodInfo OriginPropertyGetter = typeof(ImmutableDictionary<TKey, TValue>)
        .GetProperty("Origin", BindingFlags.Instance | BindingFlags.NonPublic).GetGetMethod(true);

    private static readonly MethodInfo AddMethod = typeof(ImmutableDictionary<TKey, TValue>)
        .GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Where(m => m.Name == "Add" && m.GetParameters().Length == 4).FirstOrDefault();

    private static readonly Type MutationResultType = AddMethod.ReturnType;

    private static readonly MethodInfo FinalizeMethod = MutationResultType
        .GetMethod("Finalize", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);

    private static readonly Func<TKey, TValue, KeyCollisionBehavior, ImmutableDictionary<TKey, TValue>, ImmutableDictionary<TKey, TValue>> AddAndFinalize = CreateAddAndFinalize();

    private static Func<TKey, TValue, KeyCollisionBehavior, ImmutableDictionary<TKey, TValue>, ImmutableDictionary<TKey, TValue>> CreateAddAndFinalize()
    {
        var method = new DynamicMethod(
            nameof(AddAndFinalize),
            typeof(ImmutableDictionary<TKey, TValue>),
            new[] { typeof(TKey), typeof(TValue), typeof(KeyCollisionBehavior), typeof(ImmutableDictionary<TKey, TValue>) },
            typeof(ImmutableDictionary<TKey, TValue>));

        var ilGen = method.GetILGenerator();
        ilGen.DeclareLocal(OriginPropertyGetter.ReturnType);
        ilGen.DeclareLocal(AddMethod.ReturnType);

        // var origin = dictionary.Origin;
        ilGen.Emit(OpCodes.Ldarg_3);
        ilGen.Emit(OpCodes.Callvirt, OriginPropertyGetter);
        ilGen.Emit(OpCodes.Stloc_0);

        // var result = Add(key, value, behavior, origin)
        ilGen.Emit(OpCodes.Ldarg_0);
        ilGen.Emit(OpCodes.Ldarg_1);
        ilGen.Emit(OpCodes.Ldarg_2);
        ilGen.Emit(OpCodes.Ldloc_0);
        ilGen.Emit(OpCodes.Call, AddMethod);
        ilGen.Emit(OpCodes.Stloc_1);

        // var newDictionary = result.Finalize(dictionary);
        ilGen.Emit(OpCodes.Ldloca_S, 1);
        ilGen.Emit(OpCodes.Ldarg_3);
        ilGen.Emit(OpCodes.Call, FinalizeMethod);

        // return newDictionary;
        ilGen.Emit(OpCodes.Ret);

        var del = method.CreateDelegate(typeof(Func<TKey, TValue, KeyCollisionBehavior, ImmutableDictionary<TKey, TValue>, ImmutableDictionary<TKey, TValue>>));
        var func = (Func<TKey, TValue, KeyCollisionBehavior, ImmutableDictionary<TKey, TValue>, ImmutableDictionary<TKey, TValue>>)del;
        return func;
    }

    public static ImmutableDictionary<TKey, TValue> Add(ImmutableDictionary<TKey, TValue> source, TKey key, TValue value, KeyCollisionBehavior behavior)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));

        return AddAndFinalize(key, value, behavior, source);
    }
}

Следует отметить одну деталь: CLR обрабатывает перечисления как целые числа, поэтому мы можем создать наше собственное перечисление KeyCollisionBehavior, которое совместимо с частным перечислением, к которому у нас нет доступа без явного преобразования.

2 голосов
/ 21 января 2020

Я думаю, вы слишком усложняете это. Из Примечания :

Если указанная пара ключ / значение уже существует в словаре, возвращается существующий экземпляр словаря.

Таким образом, мы можем определить, присутствовал ли данный ключ / значение в словаре, посмотрев, возвращает ли метод Add ту же ссылку на словарь.

In C#:

public static ImmutableDictionary<TKey, TValue> AddAndThrowIfAlreadyPresent<TKey, TValue>(
    ImmutableDictionary<TKey, TValue> dict,
    TKey key,
    TValue value)
{
    // null checks, etc
    var newDict = dict.Add(key, value);
    if (newDict == dict)
        throw new ArgumentException($"An element with the same key and value already exists. Key: {key}");
    return newDict;
}
1 голос
/ 23 января 2020

Это еще один подход, который делает то же самое, что и ответ @ Майка, но вместо этого использует скомпилированные выражения (которые немного легче читать и писать):

enum KeyCollisionBehavior
{
    SetValue = 0,
    Skip = 1,
    ThrowIfValueDifferent = 2,
    ThrowAlways = 3,
}

internal static class ImmutableDictionaryHelper<TKey, TValue>
{
    private static readonly Func<TKey, TValue, KeyCollisionBehavior, ImmutableDictionary<TKey, TValue>, ImmutableDictionary<TKey, TValue>> AddAndFinalize = CreateAddAndFinalize();

    private static Func<TKey, TValue, KeyCollisionBehavior, ImmutableDictionary<TKey, TValue>, ImmutableDictionary<TKey, TValue>> CreateAddAndFinalize()
    {
        var originPropertyGetter = typeof(ImmutableDictionary<TKey, TValue>)
            .GetProperty("Origin", BindingFlags.Instance | BindingFlags.NonPublic).GetGetMethod(true);
        var addMethod = typeof(ImmutableDictionary<TKey, TValue>)
            .GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Where(m => m.Name == "Add" && m.GetParameters().Length == 4).FirstOrDefault();
        var finalizeMethod = addMethod.ReturnType
            .GetMethod("Finalize", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);

        var key = Expression.Parameter(typeof(TKey), "key");
        var value = Expression.Parameter(typeof(TValue), "value");
        var behavior = Expression.Parameter(typeof(KeyCollisionBehavior), "behavior");
        var dictionary = Expression.Parameter(typeof(ImmutableDictionary<TKey, TValue>), "dictionary");

        // var convertedBehavior = (ImmutableDictionary<TKey, TValue>.KeyCollisionBehavior)behavior
        var convertedBehavior = Expression.Convert(behavior, addMethod.GetParameters()[2].ParameterType);
        // var origin = dictionary.Origin;
        var origin = Expression.Property(dictionary, originPropertyGetter);
        // var result = Add(key, value, behavior, origin)
        var result = Expression.Call(addMethod, key, value, convertedBehavior, origin);
        // var newDictionary = result.Finalize(dictionary);
        var newDictionary = Expression.Call(result, finalizeMethod, dictionary);

        var func = Expression.Lambda<Func<TKey, TValue, KeyCollisionBehavior, ImmutableDictionary<TKey, TValue>, ImmutableDictionary<TKey, TValue>>>(
            newDictionary, key, value, behavior, dictionary).Compile();
        return func;
    }

    public static ImmutableDictionary<TKey, TValue> Add(ImmutableDictionary<TKey, TValue> source, TKey key, TValue value, KeyCollisionBehavior behavior)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));

        return AddAndFinalize(key, value, behavior, source);
    }
}
...