Why does fastmember not seem faster than reflection here?
Asked Answered
G

1

8

(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;
        }
    }
Gymnasiarch answered 1/10, 2013 at 19:35 Comment(0)
G
6

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>));
    }
}
Gymnasiarch answered 1/10, 2013 at 19:35 Comment(4)
Hi, I m the person that asked this originally. First of thanks for looking into this and taking the time to answer and give an alternative. The thing is, in the scenario I have I don't know the types that will come to be validated before hand really so best I can do is I can cache them as I go, but given the type of application I have this could be a problem, I like your crazy meta programming solution :D. CheersGoddord
Does FastMember internally cache the member accessor (setter/getter) delegates per every type? If not which class/instance does the IL generation here (that I should cache) ?Wallboard
@Wallboard yes, it doesGymnasiarch
Alright, thank you. That was clear in your example, should have noticed :)Wallboard

© 2022 - 2024 — McMap. All rights reserved.