76 votes

C # non-boxing conversion de générique en int?

Étant donné un paramètre générique TEnum qui sera toujours un type enum, est-il possible de jeter de TEnum int sans boxing/unboxing?

Voir cet exemple de code. Cela va de la boîte/unbox la valeur inutilement.

private int Foo<TEnum>(TEnum value)
    where TEnum : struct  // C# does not allow enum constraint
{
    return (int) (ValueType) value;
}

Le C# ci-dessus est la version en mode compilé à la suite de l'IL (note boxing et unboxing opcodes):

.method public hidebysig instance int32  Foo<valuetype 
    .ctor ([mscorlib]System.ValueType) TEnum>(!!TEnum 'value') cil managed
{
  .maxstack  8
  IL_0000:  ldarg.1
  IL_0001:  box        !!TEnum
  IL_0006:  unbox.any  [mscorlib]System.Int32
  IL_000b:  ret
}

Enum conversion a été largement traité sur le cas, mais je ne pouvais pas trouver une discussion traitant de ce cas spécifique.

63voto

nawfal Points 13500

Ceci est similaire à réponses postées ici, mais utilise l'expression arbres émettent il à coulé entre les types. Expression.Convert fait le tour. La compilation des délégué (caster) est mis en cache par un intérieur de classe statique. Depuis la source de l'objet peut être déduite à partir de l'argument, je suppose qu'il offre nettoyeur appel. Par exemple, un générique contexte:

static int Generic<T>(T t)
{
    int variable = -1;

    // may be a type check - if(...
    variable = CastTo<int>.From(t);

    return variable;
}

La classe:

/// <summary>
/// Class to cast to type <see cref="T"/>
/// </summary>
/// <typeparam name="T">Target type</typeparam>
public static class CastTo<T>
{
    /// <summary>
    /// Casts <see cref="S"/> to <see cref="T"/>. 
    /// This does not cause boxing for value types. 
    /// Useful in generic methods
    /// </summary>
    /// <typeparam name="S">Source type to cast from. Usually a generic type.</typeparam>
    public static T From<S>(S s)
    {
        return Cache<S>.caster(s);
    }    

    static class Cache<S>
    {
        internal static readonly Func<S, T> caster = Get();

        static Func<S, T> Get()
        {
            var p = Expression.Parameter(typeof(S));
            var c = Expression.ConvertChecked(p, typeof(T));
            return Expression.Lambda<Func<S, T>>(c, p).Compile();
        }
    }
}

Vous pouvez remplacer l' caster func avec d'autres implémentations. Je vais comparer les performances de quelques-uns:

direct object casting, ie, (T)(object)S

caster1 = (Func<T, T>)(x => x) as Func<S, T>;

caster2 = Delegate.CreateDelegate(typeof(Func<S, T>), ((Func<T, T>)(x => x)).Method) as Func<S, T>;

caster3 = my implementation above

caster4 = EmitConverter();
static Func<S, T> EmitConverter()
{
    var method = new DynamicMethod(string.Empty, typeof(T), new[] { typeof(S) });
    var il = method.GetILGenerator();

    il.Emit(OpCodes.Ldarg_0);
    if (typeof(S) != typeof(T))
    {
        il.Emit(OpCodes.Conv_R8);
    }
    il.Emit(OpCodes.Ret);

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

Boîte de moulages:

  1. int de int

    objet de la coulée -> 42 ms
    caster1 -> 102 ms
    caster2 -> 102 ms
    caster3 -> 90 ms
    caster4 -> 101 ms

  2. int de int?

    objet de la coulée -> 651 ms
    caster1 -> fail
    caster2 -> fail
    caster3 -> 109 ms
    caster4 -> fail

  3. int? de int

    objet de la coulée -> 1957 ms
    caster1 -> fail
    caster2 -> fail
    caster3 -> 124 ms
    caster4 -> fail

  4. enum de int

    objet de la coulée -> 405 ms
    caster1 -> fail
    caster2 -> 102 ms
    caster3 -> 78 ms
    caster4 -> fail

  5. int de enum

    objet de la coulée -> 370 ms
    caster1 -> fail
    caster2 -> 93 ms
    caster3 -> 87 ms
    caster4 -> fail

  6. int? de enum

    objet de la coulée -> 2340 ms
    caster1 -> fail
    caster2 -> fail
    caster3 -> 258 ms
    caster4 -> fail

  7. enum? de int

    objet de la coulée -> 2776 ms
    caster1 -> fail
    caster2 -> fail
    caster3 -> 131 ms
    caster4 -> fail


Expression.Convert met directement en fonte de type de la source à la cible, donc il peut travailler explicites et implicites jette (pour ne pas mentionner la référence plâtres). Donc, cela laisse la place pour la manipulation de la coulée qui est par ailleurs possible que lorsque la non-boîte (c'est à dire, une méthode générique si vous n' (TTarget)(object)(TSource) il va exploser si elle n'est pas de l'identité de conversion (comme dans la section précédente) ou à la conversion de référence (comme indiqué à la section ultérieure)). Donc, je vais les inclure dans les tests.

Non encadré jette:

  1. int de double

    objet de la coulée -> fail
    caster1 -> fail
    caster2 -> fail
    caster3 -> 109 ms
    caster4 -> 118 ms

  2. enum de int?

    objet de la coulée -> fail
    caster1 -> fail
    caster2 -> fail
    caster3 -> 93 ms
    caster4 -> fail

  3. int de enum?

    objet de la coulée -> fail
    caster1 -> fail
    caster2 -> fail
    caster3 -> 93 ms
    caster4 -> fail

  4. enum? de int?

    objet de la coulée -> fail
    caster1 -> fail
    caster2 -> fail
    caster3 -> 121 ms
    caster4 -> fail

  5. int? de enum?

    objet de la coulée -> fail
    caster1 -> fail
    caster2 -> fail
    caster3 -> 120 ms
    caster4 -> fail

Pour le fun, j'ai testé un peu de type de référence des conversions:

  1. PrintStringProperty de string (représentation, modification)

    objet de la coulée -> fail (tout à fait évident, puisqu'il n'est pas précipités sur le type d'origine)
    caster1 -> fail
    caster2 -> fail
    caster3 -> 315 ms
    caster4 -> fail

  2. string de object (représentation de la préservation de conversion de référence)

    objet de la coulée -> 78 ms
    caster1 -> fail
    caster2 -> fail
    caster3 -> 322 ms
    caster4 -> fail

Testé comme ceci:

static void TestMethod<T>(T t)
{
    CastTo<int>.From(t); //computes delegate once and stored in a static variable

    int value = 0;
    var watch = Stopwatch.StartNew();
    for (int i = 0; i < 10000000; i++) 
    {
        value = (int)(object)t; 

        // similarly value = CastTo<int>.From(t);

        // etc
    }
    watch.Stop();
    Console.WriteLine(watch.Elapsed.TotalMilliseconds);
}

Note:

  1. Mon estimation est que, à moins que vous exécutez au moins une centaine de milliers de fois, c'est pas la peine, et vous avez presque rien à vous soucier de la boxe. Vous l'esprit la mise en cache des délégués a un impact sur la mémoire. Mais au-delà de cette limite, l'amélioration de la vitesse est très importante, surtout quand il s'agit de la coulée impliquant nullable.

  2. Mais le véritable avantage de l' CastTo<T> classe, c'est quand il permet de moulages qui sont possibles non encadré, comme (int)double dans un contexte générique. Comme tel, (int)(object)double d'échec dans ces scénarios.

  3. Je l'ai utilisé, Expression.ConvertChecked au lieu de Expression.Convert alors que l'arithmétique, des débordements et des underflows sont vérifiées (c'est à dire les résultats de l'exception). Car il est généré lors de l'exécution, et les paramètres enregistrés sont un moment de la compilation chose, il n'y a aucun moyen de savoir l'objet d'contexte de l'appel de code. C'est quelque chose que vous avez à vous de décider. Choisissez-en une, ou de fournir une surcharge pour les deux (mieux).

  4. Si une distribution n'existe pas de TSource de TTarget, exception est levée alors que le délégué est compilé. Si vous souhaitez un comportement différent, comme obtenir une valeur par défaut de TTarget, vous pouvez vérifier la compatibilité du type de l'utilisation de la réflexion avant de compiler délégué. Vous avez le plein contrôle sur le code généré. Sa va être extrêmement compliqué cependant, vous devez vérifier la référence de compatibilité (IsSubClassOf, IsAssignableFrom), opérateur de conversion de l'existence (va être hacky), et même, pour certains, construit dans le type de la convertibilité entre les types primitifs. Va être extrêmement hacky. Plus facile est de prendre de l'exception et de retourner la valeur par défaut de délégué basé sur ConstantExpression. Tout en indiquant une possibilité que vous pouvez imiter le comportement d' as mot-clé qui ne marche pas à jeter. De son mieux pour rester loin de lui et s'en tenir à la convention.

38voto

Michael B Points 3222

Je sais je suis en retard à la fête, mais si vous avez juste besoin de faire d'un coffre-fort jeté comme cela, vous pouvez utiliser les éléments suivants à l'aide de Delegate.CreateDelegate:

public static int Identity(int x){return x;}
// later on..
Func<int,int> identity = Identity;
Delegate.CreateDelegate(typeof(Func<int,TEnum>),identity.Method) as Func<int,TEnum>

maintenant, sans écrire Reflection.Emit ou des arbres d'expression vous avez une méthode qui vous permet de convertir int enum sans la boxe ou l'unboxing. Notez que TEnum ici doit avoir un type sous-jacent d' int ou cela va lever une exception en disant qu'il ne peut pas être lié.

Edit: Une autre méthode qui fonctionne aussi et peut-être un peu moins d'écrire...

Func<TEnum,int> converter = EqualityComparer<TEnum>.Default.GetHashCode;

Cela fonctionne pour convertir votre 32bit ou moins enum à partir d'un TEnum à un int. Non pas l'inverse. Dans .Net 3.5+, l' EnumEqualityComparer est optimisé pour l'essentiel à transformer cela en un retour (int)value;

Vous payez les frais généraux de l'utilisation d'un délégué, mais il sera certainement mieux que la boxe.

19voto

Drew Noakes Points 69288

Je ne suis pas sûr que c'est possible en C# sans l'aide de la Réflexion.En émettent. Si vous utilisez de la Réflexion.Émettre, vous pouvez charger la valeur de l'enum sur la pile, puis le traiter comme si c'est un int.

Vous devez écrire beaucoup de code mais, si vous voulez vérifier si vous aurez vraiment améliorer les performances dans ce domaine.

Je crois que l'équivalent IL serait:

.method public hidebysig instance int32  Foo<valuetype 
    .ctor ([mscorlib]System.ValueType) TEnum>(!!TEnum 'value') cil managed
{
  .maxstack  8
  IL_0000:  ldarg.1
  IL_000b:  ret
}

Notez que ce serait un échec si votre enum dérivé d' long (un entier de 64 bits.)

MODIFIER

Une autre pensée sur cette approche. De la réflexion.Émettent pouvez créer la méthode ci-dessus, mais la seule façon que vous auriez de la liaison à il serait par l'intermédiaire d'un appel virtuel (c'est à dire qu'il met en œuvre au moment de la compilation connue de l'interface/un résumé de ce que vous pourriez appeler) ou un appel indirect (c'est à dire par l'intermédiaire d'un délégué de l'invocation). J'imagine que ces deux scénarios serait plus lente que la surcharge de boxing/unboxing de toute façon.

Aussi, n'oubliez pas que le JIT est pas idiot et peut prendre soin de cela pour vous. (EDIT voir Eric Lippert commentaire sur la question d'origine -- il dit la gigue n'a pas actuellement d'effectuer cette optimisation.)

Comme avec tous les problèmes connexes: mesurez, mesurez, mesurez!

4voto

NSGaga Points 7386

...Je suis même "plus tard": )

mais juste pour s'étendre sur le post précédent (Michael B), qui a fait tout le travail intéressant

et m'a intéressé dans la fabrication d'une enveloppe d'un cas générique (si vous voulez jeter générique enum en fait)

...et optimisé un peu... (note: le point principal est d'utiliser " que " sur Func<>/délégués au lieu - comme Enum, les types de valeur ne le permettent pas)

public static class Identity<TEnum, T>
{
    public static readonly Func<T, TEnum> Cast = (Func<TEnum, TEnum>)((x) => x) as Func<T, TEnum>;
}

...et vous pouvez l'utiliser comme ça...

enum FamilyRelation { None, Father, Mother, Brother, Sister, };
class FamilyMember
{
    public FamilyRelation Relation { get; set; }
    public FamilyMember(FamilyRelation relation)
    {
        this.Relation = relation;
    }
}
class Program
{
    static void Main(string[] args)
    {
        FamilyMember member = Create<FamilyMember, FamilyRelation>(FamilyRelation.Sister);
    }
    static T Create<T, P>(P value)
    {
        if (typeof(T).Equals(typeof(FamilyMember)) && typeof(P).Equals(typeof(FamilyRelation)))
        {
            FamilyRelation rel = Identity<FamilyRelation, P>.Cast(value);
            return (T)(object)new FamilyMember(rel);
        }
        throw new NotImplementedException();
    }
}

...for (int) - juste (int)rel

3voto

Cecil Has a Name Points 3385

Je suppose que vous pouvez toujours utiliser System.Reflection.Emit pour créer une méthode dynamique et émettre les instructions qui le font sans boxe, bien que cela puisse être invérifiable.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X