.netfastmember

Why does fastmember not seem faster than reflection here?


(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;
        }
    }

Solution

  • 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>));
        }
    }