Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
435 views
in Technique[技术] by (71.8m points)

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

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 will box/unbox the value unnecessarily.

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

The above C# is release-mode compiled to the following IL (note boxing and 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 has been treated extensively on SO, but I could not find a discussion addressing this specific case.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...