Почему быстрый член не кажется здесь быстрее, чем отражение?

(это через вопрос в твиттере, переписан здесь с разрешения)

Я пытаюсь быстро проверить некоторые объекты (для проверки на нулевые значения), и я подумал, что FastMember мог бы помочь - однако в тестах, показанных ниже, я вижу гораздо худшую производительность. Я делаю что-то неправильно?

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

1 ответ

Решение

Основная проблема заключается в том, что вы включаете единовременную стоимость метапрограммирования во время. FastMember несет некоторые накладные расходы, обрабатывая типы и генерируя подходящий IL, и, конечно: все уровни генерации IL тогда нуждаются в JIT. Так что да, используется один раз: FastMember может показаться дороже. И действительно, вы бы не использовали что-то вроде FastMember, если бы собирались выполнить эту работу только один раз (рефлексия была бы в порядке). Хитрость заключается в том, чтобы сделать все один раз (в обоих тестах) вне времени, чтобы производительность первого запуска не влияла на результаты. А в производительности вам обычно нужно запускать вещи намного более одного раза. Вот моя установка:

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

Что показывает fast-member немного быстрее, но не так сильно, как хотелось бы - 600 мс (отражение) против 200 мс (FastMember); вполне возможно, что 1.0.11 слишком сильно изменяет положение в сторону больших классов (использование 1.0.10 занимает всего 130 мс). Я мог бы выпустить 1.0.12, которая использует различные стратегии для небольших и больших классов для компенсации.

Тем не мение! В вашем случае, если все, что вы хотите проверить, это nullЯ бы на самом деле серьезно подумал об оптимизации этого случая через IL.

Например, следующее займет всего 45 мсек для того же теста:

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

с помощью:

public static bool IsValidMetaprogramming<T>(T actions)
{
    return !NullTester<T>.HasNulls(actions);
}

и немного сумасшедший метапрограммирующий код, который делает тест для любого данного T все в одном месте:

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>));
    }
}
Другие вопросы по тегам