The main problem here is that you are including the 1-off cost of meta-programming inside the timing. FastMember incurs some overhead while it processes the types and generates suitable IL, and of course: all of the IL generation layers then need JIT on top of that. So yes, used once : FastMember may appear more expensive. And indeed, you wouldn't use something like FastMember if you were only going to do this work once (reflection would be fine). The trick is to do everything once (in both tests) outside the timing, so that the first run performance isn't biasing the results. And, in performance, you usually need to run things a lot more than once. Here's my rig:
const int CYCLES = 500000;
[Test]
public void Benchmark_speed()
{
var player = CreateValidStuffToTest();
ValidateStuff.IsValid(player); // warm up
var _stopwatch = Stopwatch.StartNew();
for (int i = 0; i < CYCLES; i++)
{
ValidateStuff.IsValid(player);
}
_stopwatch.Stop();
Console.WriteLine(_stopwatch.Elapsed);
Console.WriteLine("Reflection: {0}ms", _stopwatch.ElapsedMilliseconds);
}
[Test]
public void When_Benchmark_fastMember()
{
var player = CreateValidStuffToTest();
ValidateStuff.IsValidFastMember(player); // warm up
var _stopwatch = Stopwatch.StartNew();
for (int i = 0; i < CYCLES; i++)
{
ValidateStuff.IsValidFastMember(player);
}
_stopwatch.Stop();
Console.WriteLine("FastMember: {0}ms", _stopwatch.ElapsedMilliseconds);
}
Which shows fast-member a fair bit faster, but not as much as I would like - 600ms (reflection) vs 200ms (FastMember); quite possibly the 1.0.11 changes biased things too much towards large classes (using 1.0.10 takes only 130ms). I might release a 1.0.12 that uses different strategies for small vs large classes to compensate.
However! In your case, if all you want to test is null
, I would actually put serious consideration into optimizing that case via IL directly.
For example, the following takes just 45ms for the same test:
[Test]
public void When_Benchmark_Metaprogramming()
{
var player = CreateValidStuffToTest();
Console.WriteLine(ValidateStuff.IsValidMetaprogramming(player)); // warm up
var _stopwatch = Stopwatch.StartNew();
for (int i = 0; i < CYCLES; i++)
{
ValidateStuff.IsValidMetaprogramming(player);
}
_stopwatch.Stop();
Console.WriteLine("Metaprogramming: {0}ms", _stopwatch.ElapsedMilliseconds);
}
using:
public static bool IsValidMetaprogramming<T>(T actions)
{
return !NullTester<T>.HasNulls(actions);
}
and some suitably crazy meta-programming code that does the test for any given T
all in one place:
static class NullTester<T>
{
public static readonly Func<T, bool> HasNulls;
static NullTester()
{
if (typeof(T).IsValueType)
throw new InvalidOperationException("Exercise for reader: value-type T");
var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
var dm = new DynamicMethod("HasNulls", typeof(bool), new[] { typeof(T) });
var il = dm.GetILGenerator();
Label next, foundNull;
foundNull = il.DefineLabel();
Dictionary<Type, LocalBuilder> locals = new Dictionary<Type, LocalBuilder>();
foreach (var prop in props)
{
if (!prop.CanRead) continue;
var getter = prop.GetGetMethod(false);
if (getter == null) continue;
if (prop.PropertyType.IsValueType
&& Nullable.GetUnderlyingType(prop.PropertyType) == null)
{ // non-nullable value-type; can never be null
continue;
}
next = il.DefineLabel();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Callvirt, getter);
if (prop.PropertyType.IsValueType)
{
// have a nullable-value-type on the stack; need
// to call HasValue, which means we need it as a local
LocalBuilder local;
if (!locals.TryGetValue(prop.PropertyType, out local))
{
local = il.DeclareLocal(prop.PropertyType);
locals.Add(prop.PropertyType, local);
}
il.Emit(OpCodes.Stloc, local);
il.Emit(OpCodes.Ldloca, local);
il.Emit(OpCodes.Call,
prop.PropertyType.GetProperty("HasValue").GetGetMethod(false));
il.Emit(OpCodes.Brtrue_S, next);
}
else
{
// is a class; fine if non-zero
il.Emit(OpCodes.Brtrue_S, next);
}
il.Emit(OpCodes.Br, foundNull);
il.MarkLabel(next);
}
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Ret);
il.MarkLabel(foundNull);
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Ret);
HasNulls = (Func<T, bool>)dm.CreateDelegate(typeof(Func<T, bool>));
}
}