(this is via a question on twitter, re-asked here with permission)
I'm trying to validate some objects quickly (to test for nulls), and I thought FastMember might be able to help - however, with the tests shown below I am seeing much worse performance. Am I doing something wrong?
public class ValidateStuffTests
{
[Test]
public void Benchmark_speed()
{
var player = CreateValidStuffToTest();
_stopwatch.Start();
CharacterActions.IsValid(player);
_stopwatch.Stop();
Console.WriteLine(_stopwatch.Elapsed);
Assert.Less(_stopwatch.ElapsedMilliseconds, 10, string.Format("IsValid took {0} mileseconds", _stopwatch.Elapsed));
}
[Test]
public void When_Benchmark_fastMember()
{
var player = CreateValidStuffToTest();
_stopwatch.Start();
CharacterActions.IsValidFastMember(player);
_stopwatch.Stop();
Assert.Less(_stopwatch.ElapsedMilliseconds, 10, string.Format("IsValid took {0} mileseconds", _stopwatch.Elapsed));
}
}
public static class ValidateStuff
{
public static bool IsValid<T>(T actions)
{
var propertyInfos = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var property in propertyInfos)
{
if (property.GetValue(actions, null) == null)
return false;
}
return true;
}
public static bool IsValidFastMember<T>(T actions)
{
var typeAccessor = TypeAccessor.Create(typeof(T));
foreach (var property in typeAccessor.GetMembers())
{
if (typeAccessor[actions, property.Name] == null)
return false;
}
return true;
}
}
Reflection will always be slower than direct calls, because you have to perform several steps to find and verify that what you're calling exists. It was always bad.... Of course sometimes you have no choice, its up to the programmer to know when those times are, and avoid it otherwise.
Reflection is 104% slower than direct access (so about twice as slow). It also takes longer to warm up.
Reflection is not THAT slow. Invoking a method by reflection is about 3 times slower than the normal way. That is no problem if you do this just once or in non-critical situations. If you use it 10'000 times in a time-critical method, I would consider to change the implementation.
Reflection requires a large amount of the type metadata to be loaded and then processed. This can result in a larger memory overhead and slower execution.
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>));
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With