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.
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 FlagsAttribute
s, 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.
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.
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.
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.
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.
[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).
static class EnumAsm
{
static readonly AssemblyBuilder __Assembly = AssemblyBuilder.DefineDynamicAssembly(
new AssemblyName { Name = nameof(EnumAsm) }, AssemblyBuilderAccess.RunAndSave);
static readonly ModuleBuilder __Module = __Assembly.DefineDynamicModule(nameof(EnumAsm) + ".dll");
static readonly TypeBuilder __Manipulator = DefineType();
static readonly MethodBase __MatchFlags = DefineMatchFlagsMethod(false);
internal static readonly Type Type = __Manipulator.CreateType();
internal static void SaveAssembly() {
__Assembly.Save(nameof(EnumAsm) + ".dll");
}
private static TypeBuilder DefineType() {
var t = __Module.DefineType("DynamicAssembly." + nameof(EnumAsm),
TypeAttributes.Public | TypeAttributes.Sealed
| TypeAttributes.Class | TypeAttributes.Abstract);
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);
var gps = m.DefineGenericParameters("TEnum");
foreach (var item in gps) {
item.SetGenericParameterAttributes(GenericParameterAttributes.NotNullableValueTypeConstraint);
item.SetBaseTypeConstraint(typeof(Enum));
}
m.SetParameters(gps[0], gps[0]);
m.SetReturnType(typeof(bool));
m.DefineParameter(1, ParameterAttributes.HasDefault, "value");
m.DefineParameter(2, ParameterAttributes.HasDefault, "flags");
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:
- Run-time reprogrammability.
- Better performance (avoiding repetitive type comparisons, reflections, etc.), sometimes the performance can go better than manually coded C# programs.
- 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:
- Usually harder to program. The compiler could not help check the correctness of your dynamic code or point out where the code goes wrong.
- The requirement of decent knowledge of IL.
- Much harder to debug, since you won't have the source code for those dynamically generated methods or types in most cases.
- Code safety may be compromised.
- 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.
- 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. - 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 Delegate
s, it could not run too faster than dynamic methods.
History
- Dec 1st, 2017: Initial publish, titled Boosting Performance with Dynamic Assemblies.
- Sept 15th, 2018: Retitled to Some Fun with Dynamic Methods and CLR (Part 1)