为什么FastMember在这里似乎不比反射更快?

8

(这是通过Twitter上的一个问题,获得了允许后,在此重新提问)

我正在尝试快速验证一些对象(以测试是否为null),我认为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个回答

6
这里的主要问题在于您将元编程的一次性成本包括在计时中。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);
}

这段内容涉及到IT技术,比较快的成员方法是使用FastMember,但效果还不够好 - 反射需要600毫秒,而FastMember只需要200毫秒;很可能是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>));
    }
}

嗨,我是最初提出这个问题的人。首先感谢您对此进行调查并花时间回答并提供替代方案。问题是,在我拥有的场景中,我真的不知道将要验证的类型,所以我能做的最好的就是在进行缓存时逐步进行缓存,但考虑到我拥有的应用程序类型,这可能会成为一个问题,我喜欢您疯狂的元编程解决方案:D。干杯 - roundcrisis
FastMember 内部是否为每个类型缓存成员访问器(setter/getter)委托?如果不是,哪个类/实例在此处生成 IL(我应该缓存它)? - nawfal
@nawfal 是的,它确实可以。 - Marc Gravell
好的,谢谢。你的例子很清楚,我应该注意到了 :) - nawfal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接