Click here to Skip to main content
15,868,016 members
Articles / Programming Languages / MSIL

Some Fun with Dynamic Methods and CLR (Part 1)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (4 votes)
14 Sep 2018CPOL7 min read 18K   188   20   33
Circumventing language obstacles and optimizing performance with Dynamic Methods, Dynamic Assemblies.

Introduction

In this article, dynamic method and dynamic assembly will be demonstrated to circumvent limitations in C# compilers. Also, a benchmark was also performed to evaluate the performance of dynamic implementations and hard-coded counterparts.

*Note: This article had been published one year ago and I retitled it to prepare for the part 2.

Background

The story began with the following code.

C#
if ((member.MemberType & System.Refleciton.MemberTypes.Method)
      == System.Refleciton.MemberTypes.Method) {
   Console.WriteLine ("The member is a method.");
}

We have been so used to use bitwise operations on Enum types marked with FlagsAttributes, like the above code, which checks whether the MemberType property of a MemberInfo instance, member.MemberType, has the bit MemberTypes.Method set.

Most of us might have tried to write a generic method to make the above code simplier. The method could look like the following at the first thought, but it never be compiled. The compiler will say that Enum could not be a constraint type.

C#
public bool HasFlag<TEnum>(this TEnum value, TEnum flag)
  where TEnum : struct, Enum
{
  return (value & flag) == flag;
}
Note:

From C# 7.3 on, the compiler allows Enum to be type constraint. However, it is still impossible to perform arithmetic operations against generic enum types.

The solutions

The similar question and answers had been posted on StackOverflow.com, which provided several wordarounds: coding with IL Support, using F#, etc. I like the IL support approach. However, when I was applying it to my projects which had quite dozens of code files, Visual Studio kept getting frozen unless the changes were undone. The F# approach was not my option at this moment, since it would require us to install the F# support to our Visual Studio and make the compiled assembly unnecessarily relied on FSharp.Core, which could be avoided.

Will there be other solutions? How do they perform? I tried the following solutions and benchmarked them.

Solution 1: IConvertible

The following piece of code was written quite a few years ago. Since Enum values implements the IConvertible interface and they were actually integers, they could be converted to UInt64 (the biggest possible value type) and perform bitwise operations on them. As a bonus, this method could also be applied on other integer types.

C#
static bool MatchFlags<TEnum>(TEnum value, TEnum flags)
  where TEnum : struct, IConvertible {
    var v = flags.ToUInt64(CultureInfo.InvariantCulture);
    return (value.ToUInt64(CultureInfo.InvariantCulture) & v) == v;
}

How about the performance? Very bad.

The benchmark was performed with BenchmarkDotNet. To keep the Enum instance from being optimized away, I set up a class and a static variable _C to hold an Enum type.

C#
static readonly C _C = new C();

class C
{
    public MemberTypes M { get; set; }
}

Then I used the handwritten calculation code for the baseline.

[Benchmark(Baseline = true)]
public void DirectCalculation() {
    _C.M = MemberTypes.Event | MemberTypes.Method;
    var b = (_C.M & MemberTypes.Method) == MemberTypes.Method;
    if (b == false) {
        throw new InvalidOperationException();
    }
}

And the following code uses the above MatchFlags method to do the calculation.

[Benchmark]
public void MatchWithConvert() {
    _C.M = MemberTypes.Event | MemberTypes.Method;
    var b = MatchFlags(_C.M, MemberTypes.Method);
    if (b == false) {
        throw new InvalidOperationException();
    }
}

A benchmark results indicated that the MatchFlags method took about 102 ns to finish the calculation where hard coded bitwise-and took less than 0.4 ns. The IConvertible solution was about 250+ times slower than the baseline.

The reason of bad performance was that, inside the type conversion of ToUInt64, many type comparisons occured, which severely slowed down the code.

Solution 2: Enum.HasFlag

The Enum.HasFlag method came with .NET 4.0, which meant that it was not available in earlier versions of .NET Frameworks.

The code benchmark was quite similar as the above.

[Benchmark]
public void MatchWithHasFlag() {
    _C.M = MemberTypes.Event | MemberTypes.Method;
    var b = _C.M.HasFlag(MemberTypes.Method);
    if (b == false) {
        throw new InvalidOperationException();
    }
}

The performance benchmark indicated that the method took about 28 ns to finish, still about 70+ times slower than the hard coded calculation. The result was not so good yet.

Solution 3: Dynamic Method

The dynamic method approach requires some decent understanding of IL and execution stacks, as well as a bit more coding.

A dynamic method is can be generated by DynamicMethod and compiled at run-time. The compilation can take considerable time and the compiled code will sit in the memory. Thus the compilation has to be done only once, and the compiled method should be cached to avoid memory leaks.

Usually I use static class to initiate the compilation and store the result. The CLR will automatically ensure that it occurs no more than twice, even if the program is running in a multi-threaded environment.

Here is the class I wrote to generate and cache the dynamic method.

C#
using System.Reflection;
using System.Reflection.Emit;

static class EnumManipulator<TEnum> where TEnum : struct, IComparable, IConvertible
{
    internal static readonly bool IsEnum = typeof(TEnum).IsEnum;
    internal static readonly Func<TEnum, TEnum, bool> MatchFlags = CreateMatchFlagsMethod();

    private static Func<TEnum, TEnum, bool> CreateMatchFlagsMethod() {
        var et = typeof(TEnum).GetEnumUnderlyingType();
        var isLong = et == typeof(long) || et == typeof(ulong);
        var m = new DynamicMethod(typeof(TEnum).Name + "MatchFlags", typeof(bool),
                    new[] { typeof(TEnum), typeof(TEnum) }, true);
        var il = m.GetILGenerator();
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Dup);
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.And);
        il.Emit(OpCodes.Ceq);
        il.Emit(OpCodes.Ret);
        return (Func<TEnum, TEnum, bool>)m.CreateDelegate(typeof(Func<TEnum, TEnum, bool>));
    }
}

The class contains two internal fields, IsEnum will check whether the type TEnum is an Enum type, and MatchFlags is a Delegate created from a dynamic method, which can be used to do the calculation.

The code used in the benchmark was listed below.

C#
[Benchmark]
public void MatchWithDynamicMethod() {
    _C.M = MemberTypes.Event | MemberTypes.Method;
    var b = EnumManipulator<TEnum>.MatchFlags(_C.M, MemberTypes.Method);
    if (b == false) {
        throw new InvalidOperationException();
    }
}
Note:

In production code, you should check whether the type passed to the generic class is Enum type with the EnumManipulator<TEnum>.IsEnum Field.

Since the generic static class EnumManipulator<TEnum> only initialize once for each TEnum type, checking whether TEnum is an Enum type will only takes place once and the result will be cached into the IsEnum field for subsequent use, and the pre-baked MatchFlags will be used to perform direct bitwise calculation on the value and flags parameter.

Because of the absence of type comparison and conversion, the benchmark result was greatly improved, showing that the dynamic method ran about 2.7 ns only, although it was still 7 times slower than direct calculation due to the overhead of delegate calling and security verification, it had run about 10 times faster than Enum.HasFlag.

Solution 4: Saved Dynamic Assembly

To eliminate the overhead of the dynamic methods and further improve the performance, I attempted to create a dynamic assembly, saved it to the disk and had the benchmark project work with it.

The following class EnumAsm was used to create such a dynamic assembly. To save the assembly to disk, create another project, or use the C# Interactive feature of Visual Studio, and have it call the SaveAssembly method (note: the dynamic assembly in memory can only be saved once, don't call that method twice, otherwise an Exception will be thrown).

C#
static class EnumAsm
{
    // define a dynamic assembly
    static readonly AssemblyBuilder __Assembly = AssemblyBuilder.DefineDynamicAssembly(
        new AssemblyName { Name = nameof(EnumAsm) }, AssemblyBuilderAccess.RunAndSave);
    // add a module in the assembly
    static readonly ModuleBuilder __Module = __Assembly.DefineDynamicModule(nameof(EnumAsm) + ".dll");
    // define the type which holds the methods
    static readonly TypeBuilder __Manipulator = DefineType();
    // define the method in the type
    static readonly MethodBase __MatchFlags = DefineMatchFlagsMethod(false);
    // create the type for run-time use
    internal static readonly Type Type = __Manipulator.CreateType();

    /// <summary>Saves the assembly to disk.</summary>
    internal static void SaveAssembly() {
        __Assembly.Save(nameof(EnumAsm) + ".dll");
    }

    private static TypeBuilder DefineType() {
        // define a static type
        var t = __Module.DefineType("DynamicAssembly." + nameof(EnumAsm),
            TypeAttributes.Public | TypeAttributes.Sealed
            | TypeAttributes.Class | TypeAttributes.Abstract);
        // mark the type, allow it to have extension methods
        t.SetCustomAttribute(new CustomAttributeBuilder(
            typeof(System.Runtime.CompilerServices.ExtensionAttribute).GetConstructor(Type.EmptyTypes),
            new object[0]));
        return t;
    }

    static MethodBuilder DefineMatchFlagsMethod(bool isLong) {
        var m = __Manipulator.DefineMethod("MatchFlags",
            MethodAttributes.Static | MethodAttributes.Public | MethodAttributes.HideBySig);
        // define a generic type argument for the method
        var gps = m.DefineGenericParameters("TEnum");
        // constraint the generic type to be: struct, Enum
        foreach (var item in gps) {
            item.SetGenericParameterAttributes(GenericParameterAttributes.NotNullableValueTypeConstraint);
            item.SetBaseTypeConstraint(typeof(Enum));
        }
        // set argument type of the method to (TEnum, TEnum)
        m.SetParameters(gps[0], gps[0]);
        m.SetReturnType(typeof(bool));
        // name the parameter of the method to (TEnum value, TEnum flags)
        m.DefineParameter(1, ParameterAttributes.HasDefault, "value");
        m.DefineParameter(2, ParameterAttributes.HasDefault, "flags");
        // mark the method to be an extension method
        m.SetCustomAttribute(new CustomAttributeBuilder(
            typeof(System.Runtime.CompilerServices.ExtensionAttribute).GetConstructor(Type.EmptyTypes),
            new object[0]));
        var il = m.GetILGenerator();
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Dup);
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.And);
        il.Emit(OpCodes.Ceq);
        il.Emit(OpCodes.Ret);
        return m;
    }
}

The above code created a type with a generic method named MatchFlags. The generic type parameter TEnum in that method would constrainted by Enum, that was an impossible outcome from the C# compiler. The corresponding C# code might look like the following.

namespace DynamicAssembly
{
    public static class EnumAsm
    {
        public static bool MatchFlags<TEnum>(this TEnum value, TEnum flags)
            where TEnum : struct, Enum
        {
            return flags == (flags & value);
        }
    }
}

I used the following code in the benchmark.

[Benchmark]
public void MatchWithSavedDynamicAssembly() {
    _C.M = MemberTypes.Event | MemberTypes.Method;
    var b = DynamicAssembly.EnumAsm.MatchFlags(_C.M, MemberTypes.Method);
    if (b == false) {
        throw new InvalidOperationException();
    }
}

Having gotten rid of the overhead of dynamic methods, the benchmark result of this solution showed the execution time was 0.4 ns, which was almost the same as the direct calculation.

Conclusion

By employing the saved dynamic assembly, the good things happened:

  1. Run-time reprogrammability.
  2. Better performance (avoiding repetitive type comparisons, reflections, etc.), sometimes the performance can go better than manually coded C# programs.
  3. Greater flexibility with buried features in the .NET runtime. Create something which could not be compiled with ordinary C# code and compiler.

The downsides of dynamic method and dynamic assembly are:

  1. Usually harder to program. The compiler could not help check the correctness of your dynamic code or point out where the code goes wrong.
  2. The requirement of decent knowledge of IL.
  3. Much harder to debug, since you won't have the source code for those dynamically generated methods or types in most cases.
  4. Code safety may be compromised.
  5. Sometimes the program will be removed by some virus killers simply for the capability of generating IL assemblies. This issue is typically applicable to dynamic assemblies.

Points of Interest

Who was the winner?

The following result was the one with the smallest Standard Deviation among all benchmarks I took on my machine.

BenchmarkDotNet=v0.10.10, OS=Windows 10 Redstone 1 [1607, Anniversary Update] (10.0.14393.1884)
Processor=Intel Core i5-5200U CPU 2.20GHz (Broadwell), ProcessorCount=4
Frequency=2143478 Hz, Resolution=466.5315 ns, Timer=TSC
  [Host]     : .NET Framework 4.6.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.2117.0
  DefaultJob : .NET Framework 4.6.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.2117.0

                        Method |        Mean |     Error |    StdDev | Scaled | ScaledSD |
------------------------------ |------------:|----------:|----------:|-------:|---------:|
              MatchWithConvert | 102.6391 ns | 0.5923 ns | 0.4946 ns | 277.76 |    34.34 |
              MatchWithHasFlag |  28.2697 ns | 0.6257 ns | 0.5547 ns |  76.50 |     9.56 |
        MatchWithDynamicMethod |   2.7176 ns | 0.0185 ns | 0.0144 ns |   7.35 |     0.91 |
 MatchWithSavedDynamicAssembly |   0.3310 ns | 0.0122 ns | 0.0102 ns |   0.90 |     0.11 |
             DirectCalculation |   0.3756 ns | 0.0551 ns | 0.0515 ns |   1.00 |     0.00 |

It was quite interesting that solution 4 (MatchWithSavedDynamicAssembly), which referenced the assembly generated from the dynamic assembly, outperformed the direct calculation code (the last one, basline). The above result did not occur by chance. Each time I ran the benchmark, solution 4 always beated the baseline with a small margin.

The following was my assumption to this phenomenon.

  1. The code size of the MatchFlags method in the generated dynamic assembly was as small as 7 bytes only. The JIT compiler might have inlined the code for its tiny size into the MatchWithSavedDynamicAssembly method.
  2. The MemberTypes.Method in the MatchFlags method generated with ILGenerator was loaded only once by OpCodes.ldarg_1 and duplicated with OpCodes.dup in the calculation stack. Whereas in the direct calculation code, the value was loaded twice (one for the & operation and the other for the == operation). This manual optimization may slightly save a little time.

How about the in-memory Dynamic Assembly?

You may wonder how the performance would be if the in-memory dynamic assembly in solution 4 was not saved to the disk, but called via method delegates, like the following code. You may try it yourself.

[Benchmark]
public void MatchWithDynamicAssembly() {
    _C.M = MemberTypes.Event | MemberTypes.Method;
    var b = EnumManipulatorCache<MemberTypes>.MatchFlags(_C.M, MemberTypes.Method);
    if (b == false) {
        throw new InvalidOperationException();
    }
}

static class EnumManipulatorCache<TEnum> where TEnum : struct, IComparable, IConvertible
{
    internal static readonly Func<TEnum, TEnum, bool> MatchFlags =
        (Func<TEnum, TEnum, bool>)Delegate.CreateDelegate(
            typeof(Func<TEnum, TEnum, bool>),
            EnumAsm.Type.GetMethod("MatchFlags").MakeGenericMethod(typeof(TEnum))
            );
}

Due to the overhead of Delegates, it could not run too faster than dynamic methods.

History

  1. Dec 1st, 2017: Initial publish, titled Boosting Performance with Dynamic Assemblies.
  2. Sept 15th, 2018: Retitled to Some Fun with Dynamic Methods and CLR (Part 1)

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
China China
I am now programming applications for the Internet of Things.

Comments and Discussions

 
QuestionEnum types do not enumerate Pin
Sergey Alexandrovich Kryukov14-Aug-23 15:14
mvaSergey Alexandrovich Kryukov14-Aug-23 15:14 
AnswerRe: Enum types do not enumerate Pin
wmjordan14-Aug-23 15:42
professionalwmjordan14-Aug-23 15:42 
AnswerEnumeration pitfalls Pin
Sergey Alexandrovich Kryukov14-Aug-23 16:03
mvaSergey Alexandrovich Kryukov14-Aug-23 16:03 
GeneralRe: Enumeration pitfalls Pin
wmjordan14-Aug-23 16:37
professionalwmjordan14-Aug-23 16:37 
AnswerNext article is about localization Pin
Sergey Alexandrovich Kryukov14-Aug-23 17:19
mvaSergey Alexandrovich Kryukov14-Aug-23 17:19 
GeneralRe: Next article is about localization Pin
wmjordan14-Aug-23 21:33
professionalwmjordan14-Aug-23 21:33 
GeneralMy vote of 5 Pin
Sergey Alexandrovich Kryukov14-Aug-23 14:02
mvaSergey Alexandrovich Kryukov14-Aug-23 14:02 
GeneralRe: My vote of 5 Pin
wmjordan14-Aug-23 15:57
professionalwmjordan14-Aug-23 15:57 
SuggestionOne more downside, but... Pin
Sergey Alexandrovich Kryukov14-Aug-23 13:57
mvaSergey Alexandrovich Kryukov14-Aug-23 13:57 
GeneralRe: One more downside, but... Pin
wmjordan14-Aug-23 15:21
professionalwmjordan14-Aug-23 15:21 
AnswerOops! But the safety is still not an additional issue Pin
Sergey Alexandrovich Kryukov14-Aug-23 15:41
mvaSergey Alexandrovich Kryukov14-Aug-23 15:41 
GeneralRe: Oops! But the safety is still not an additional issue Pin
wmjordan14-Aug-23 15:51
professionalwmjordan14-Aug-23 15:51 
AnswerSafe Reflection way Pin
Sergey Alexandrovich Kryukov14-Aug-23 17:28
mvaSergey Alexandrovich Kryukov14-Aug-23 17:28 
GeneralRe: Safe Reflection way Pin
wmjordan14-Aug-23 21:39
professionalwmjordan14-Aug-23 21:39 
AnswerI cannot see unsafe Pin
Sergey Alexandrovich Kryukov14-Aug-23 22:01
mvaSergey Alexandrovich Kryukov14-Aug-23 22:01 
GeneralRe: I cannot see unsafe Pin
wmjordan14-Aug-23 22:25
professionalwmjordan14-Aug-23 22:25 
AnswerNo, it does not depend Pin
Sergey Alexandrovich Kryukov15-Aug-23 4:13
mvaSergey Alexandrovich Kryukov15-Aug-23 4:13 
GeneralRe: No, it does not depend Pin
wmjordan15-Aug-23 15:31
professionalwmjordan15-Aug-23 15:31 
AnswerRe: No, it does not depend Pin
Sergey Alexandrovich Kryukov15-Aug-23 15:44
mvaSergey Alexandrovich Kryukov15-Aug-23 15:44 
Questiona very interesting article, but ... Pin
BillWoodruff4-Dec-17 8:52
professionalBillWoodruff4-Dec-17 8:52 
AnswerRe: a very interesting article, but ... Pin
wmjordan4-Dec-17 13:34
professionalwmjordan4-Dec-17 13:34 
GeneralRe: a very interesting article, but ... Pin
BillWoodruff6-Dec-17 7:04
professionalBillWoodruff6-Dec-17 7:04 
AnswerFirst of all, Data Contract Pin
Sergey Alexandrovich Kryukov14-Aug-23 13:45
mvaSergey Alexandrovich Kryukov14-Aug-23 13:45 
GeneralRe: First of all, Data Contract Pin
wmjordan14-Aug-23 15:04
professionalwmjordan14-Aug-23 15:04 
AnswerRe: First of all, Data Contract Pin
Sergey Alexandrovich Kryukov14-Aug-23 15:19
mvaSergey Alexandrovich Kryukov14-Aug-23 15:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.