C# non-boxing conversion of generic enum to int?

前端 未结 9 1797
庸人自扰
庸人自扰 2020-12-04 07:31

Given a generic parameter TEnum which always will be an enum type, is there any way to cast from TEnum to int without boxing/unboxing?

See this example code. This w

相关标签:
9条回答
  • 2020-12-04 07:46

    If you'd like to speed up conversion, restricted to use unsafe code and can't emit IL you may want to consider to make generic class as abstract and implement conversion in derived classes. For example, when you code for Unity engine you probably want to build IL2CPP targets which are not compatible with emit. Here is an example of how it can be implemented:

    // Generic scene resolver is abstract and requires
    // to implement enum to index conversion
    public abstract class SceneResolver<TSceneTypeEnum> : ScriptableObject
        where TSceneTypeEnum : Enum
    {
        protected ScenePicker[] Scenes;
    
        public string GetScenePath ( TSceneTypeEnum sceneType )
        {
            return Scenes[SceneTypeToIndex( sceneType )].Path;
        }
    
        protected abstract int SceneTypeToIndex ( TSceneTypeEnum sceneType );
    }
    
    // Here is some enum for non-generic class
    public enum SceneType
    {
    }
    
    // Some non-generic implementation
    public class SceneResolver : SceneResolver<SceneType>
    {
        protected override int SceneTypeToIndex ( SceneType sceneType )
        {
            return ( int )sceneType;
        }
    }
    
    

    I tested boxing vs. virtual method and got 10x speed up for virtual method approach on macOS for both Mono and IL2CPP targets.

    0 讨论(0)
  • 2020-12-04 07:50

    This is similar to answers posted here, but uses expression trees to emit il to cast between types. Expression.Convert does the trick. The compiled delegate (caster) is cached by an inner static class. Since source object can be inferred from the argument, I guess it offers cleaner call. For e.g. a generic context:

    static int Generic<T>(T t)
    {
        int variable = -1;
    
        // may be a type check - if(...
        variable = CastTo<int>.From(t);
    
        return variable;
    }
    

    The class:

    /// <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);
        }    
    
        private static class Cache<S>
        {
            public static readonly Func<S, T> caster = Get();
    
            private 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();
            }
        }
    }
    

    You can replace the caster func with other implementations. I will compare performance of a few:

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

    Boxed casts:

    1. int to int

      object casting -> 42 ms
      caster1 -> 102 ms
      caster2 -> 102 ms
      caster3 -> 90 ms
      caster4 -> 101 ms

    2. int to int?

      object casting -> 651 ms
      caster1 -> fail
      caster2 -> fail
      caster3 -> 109 ms
      caster4 -> fail

    3. int? to int

      object casting -> 1957 ms
      caster1 -> fail
      caster2 -> fail
      caster3 -> 124 ms
      caster4 -> fail

    4. enum to int

      object casting -> 405 ms
      caster1 -> fail
      caster2 -> 102 ms
      caster3 -> 78 ms
      caster4 -> fail

    5. int to enum

      object casting -> 370 ms
      caster1 -> fail
      caster2 -> 93 ms
      caster3 -> 87 ms
      caster4 -> fail

    6. int? to enum

      object casting -> 2340 ms
      caster1 -> fail
      caster2 -> fail
      caster3 -> 258 ms
      caster4 -> fail

    7. enum? to int

      object casting -> 2776 ms
      caster1 -> fail
      caster2 -> fail
      caster3 -> 131 ms
      caster4 -> fail


    Expression.Convert puts a direct cast from source type to target type, so it can work out explicit and implicit casts (not to mention reference casts). So this gives way for handling casting which is otherwise possible only when non-boxed (ie, in a generic method if you do (TTarget)(object)(TSource) it will explode if it is not identity conversion (as in previous section) or reference conversion (as shown in later section)). So I will include them in tests.

    Non-boxed casts:

    1. int to double

      object casting -> fail
      caster1 -> fail
      caster2 -> fail
      caster3 -> 109 ms
      caster4 -> 118 ms

    2. enum to int?

      object casting -> fail
      caster1 -> fail
      caster2 -> fail
      caster3 -> 93 ms
      caster4 -> fail

    3. int to enum?

      object casting -> fail
      caster1 -> fail
      caster2 -> fail
      caster3 -> 93 ms
      caster4 -> fail

    4. enum? to int?

      object casting -> fail
      caster1 -> fail
      caster2 -> fail
      caster3 -> 121 ms
      caster4 -> fail

    5. int? to enum?

      object casting -> fail
      caster1 -> fail
      caster2 -> fail
      caster3 -> 120 ms
      caster4 -> fail

    For the fun of it, I tested a few reference type conversions:

    1. PrintStringProperty to string (representation changing)

      object casting -> fail (quite obvious, since it is not cast back to original type)
      caster1 -> fail
      caster2 -> fail
      caster3 -> 315 ms
      caster4 -> fail

    2. string to object (representation preserving reference conversion)

      object casting -> 78 ms
      caster1 -> fail
      caster2 -> fail
      caster3 -> 322 ms
      caster4 -> fail

    Tested like this:

    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. My estimate is that unless you run this at least a hundred thousand times, it's not worth it, and you have almost nothing to worry about boxing. Mind you caching delegates has a hit on memory. But beyond that limit, the speed improvement is significant, especially when it comes to casting involving nullables.

    2. But the real advantage of the CastTo<T> class is when it allows casts that are possible non-boxed, like (int)double in a generic context. As such (int)(object)double fails in these scenarios.

    3. I have used Expression.ConvertChecked instead of Expression.Convert so that arithmetic overflows and underflows are checked (ie results in exception). Since il is generated during run time, and checked settings are a compile time thing, there is no way you can know the checked context of calling code. This is something you have to decide yourself. Choose one, or provide overload for both (better).

    4. If a cast doesn't exist from TSource to TTarget, exception is thrown while the delegate is compiled. If you want a different behaviour, like get a default value of TTarget, you can check type compatibility using reflection before compiling delegate. You have the full control of the code being generated. Its going to be extremely tricky though, you have to check for reference compatibility (IsSubClassOf, IsAssignableFrom), conversion operator existence (going to be hacky), and even for some built in type convertibility between primitive types. Going to be extremely hacky. Easier is to catch exception and return default value delegate based on ConstantExpression. Just stating a possibility that you can mimic behaviour of as keyword which doesnt throw. Its better to stay away from it and stick to convention.

    0 讨论(0)
  • 2020-12-04 07:53

    ...I'm even 'later' : )

    but just to extend on the previous post (Michael B), which did all the interesting work

    and got me interested into making a wrapper for a generic case (if you want to cast generic to enum actually)

    ...and optimized a bit... (note: the main point is to use 'as' on Func<>/delegates instead - as Enum, value types do not allow it)

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

    ...and you can use it like this...

    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) - just (int)rel

    0 讨论(0)
  • 2020-12-04 07:54

    Here's a very straight forward solution with C# 7.3's unmanaged generic type constraint:

        using System;
        public static class EnumExtensions<TEnum> where TEnum : unmanaged, Enum
        {
            /// <summary>
            /// Converts a <typeparam name="TEnum"></typeparam> into a <typeparam name="TResult"></typeparam>
            /// through pointer cast.
            /// Does not throw if the sizes don't match, clips to smallest data-type instead.
            /// So if <typeparam name="TResult"></typeparam> is smaller than <typeparam name="TEnum"></typeparam>
            /// bits that cannot be captured within <typeparam name="TResult"></typeparam>'s size will be clipped.
            /// </summary>
            public static TResult To<TResult>( TEnum value ) where TResult : unmanaged
            {
                unsafe
                {
                    if( sizeof(TResult) > sizeof(TEnum) )
                    {
                        // We might be spilling in the stack by taking more bytes than value provides,
                        // alloc the largest data-type and 'cast' that instead.
                        TResult o = default;
                        *((TEnum*) & o) = value;
                        return o;
                    }
                    else
                    {
                        return * (TResult*) & value;
                    }
                }
            }
    
            /// <summary>
            /// Converts a <typeparam name="TSource"></typeparam> into a <typeparam name="TEnum"></typeparam>
            /// through pointer cast.
            /// Does not throw if the sizes don't match, clips to smallest data-type instead.
            /// So if <typeparam name="TEnum"></typeparam> is smaller than <typeparam name="TSource"></typeparam>
            /// bits that cannot be captured within <typeparam name="TEnum"></typeparam>'s size will be clipped.
            /// </summary>
            public static TEnum From<TSource>( TSource value ) where TSource : unmanaged
            {
                unsafe
                {
    
                    if( sizeof(TEnum) > sizeof(TSource) )
                    {
                        // We might be spilling in the stack by taking more bytes than value provides,
                        // alloc the largest data-type and 'cast' that instead.
                        TEnum o = default;
                        *((TSource*) & o) = value;
                        return o;
                    }
                    else
                    {
                        return * (TEnum*) & value;
                    }
                }
            }
        }
    
    

    Requires unsafe toggle in your project configuration.

    Usage:

    int intValue = EnumExtensions<YourEnumType>.To<int>( yourEnumValue );
    

    Edit: Replaced Buffer.MemoryCopy by simple pointer cast from dahall's suggestion.

    0 讨论(0)
  • 2020-12-04 07:56

    I'm not sure that this is possible in C# without using Reflection.Emit. If you use Reflection.Emit, you could load the value of the enum onto the stack and then treat it as though it's an int.

    You have to write quite a lot of code though, so you'd want to check whether you'll really gain any performance in doing this.

    I believe the equivalent IL would be:

    .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
    }
    

    Note that this would fail if your enum derived from long (a 64 bit integer.)

    EDIT

    Another thought on this approach. Reflection.Emit can create the method above, but the only way you'd have of binding to it would be via a virtual call (i.e. it implements a compile-time known interface/abstract that you could call) or an indirect call (i.e. via a delegate invocation). I imagine that both of these scenarios would be slower than the overhead of boxing/unboxing anyway.

    Also, don't forget that the JIT is not dumb and may take care of this for you. (EDIT see Eric Lippert's comment on the original question -- he says the jitter does not currently perform this optimisation.)

    As with all performance related issues: measure, measure, measure!

    0 讨论(0)
  • 2020-12-04 07:57

    I know I'm way late to the party, but if you just need to do a safe cast like this you can use the following using 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>
    

    now without writing Reflection.Emit or expression trees you have a method that will convert int to enum without boxing or unboxing. Note that TEnum here must have an underlying type of int or this will throw an exception saying it cannot be bound.

    Edit: Another method that works too and might be a little less to write...

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

    This works to convert your 32bit or less enum from a TEnum to an int. Not the other way around. In .Net 3.5+, the EnumEqualityComparer is optimized to basically turn this into a return (int)value;

    You are paying the overhead of using a delegate, but it certainly will be better than boxing.

    0 讨论(0)
提交回复
热议问题