Search code examples
c#.netreflectiondelegatesdynamictype

Injecting generic getters and setters to get better performance than reflection


Im trying to write a high-level network library (mostly for fun) in witch the user can easly define their packets by deriving a class. This way parsing messages is done verry easy. The user packet should only contain basic value types.

In order to do this I need to acces each field of every user defined packet. This problem can easly be done with reflection, but since reflection is verry slow I can not use it. To do this fast I made a class to inject getters and setters for each user defined packet field at runtime (found this somewhere on StackOverflow). The getters are Func<UserDefinedPacket, fieldType> and setters are Action<UserDefinedPAcket, setValue>.

Here is the problem: because the user packets are defined in another assembly I can not possibly know the type of the getters or setters at compile time. The most derived class I can use for getters and setters is Delegate. This means I can only use DynamicInvoke so I get to reflection again...but because I know the methods are actualy Funcs and Actions but can't actually cast them I can use the dynamic type and call Invoke. Dynamic type improved performance by about 5 times but setting and getting a value is still slow compared to normal field access (which is about 100 times more fast) making this library unusable.

Ultimately I want to be able to convert the user defined class to an array of bytes. Also I dont want to serialize the class because serialization includes some junk data that I dont really want to send through the network and serialization appears to be quite slow.

Here is the question: can I actually make the GetVaue and SetValue methods any faster? I tried casting the delegates to the needed functions/actions (moved the custom packet class in the library so this isn't good) and the setters took about 50ms instead of 300ms. I was hoping to get that performance for generic packets.

namespace MirrorNet
{
    // Base class for packets
    // This will be derived by users and should only contain basic type fields
    public class UserPacket
    {
    }

public class MirrorNetManager
{
    private static MirrorNetManager instance = new MirrorNetManager();

    public static MirrorNetManager Instance
    {
        get { return instance; }
    }

    // Dictionary: packetType -> field -> getter | setter
    private class PropertyDictionary : Dictionary<Type, Dictionary<FieldInfo, Delegate>>
    {
    }

    private Dictionary<int, PacketConstructor> m_packetConstructors = new Dictionary<int, PacketConstructor>();
    private PropertyDictionary m_packetFieldGetters = new PropertyDictionary();
    private PropertyDictionary m_packetFieldSetters = new PropertyDictionary();

    public void SetValue(UserPacket packet, FieldInfo field, object value)
    {
        var setDelegate = m_packetFieldSetters[packet.GetType()][field];

        dynamic setAction = setDelegate; //Convert.ChangeType(setDelegate, setDelegate.GetType());
        dynamic setObject = packet;      //Convert.ChangeType(packet, packet.GetType());
        dynamic setValue  = value;       //Convert.ChangeType(value, value.GetType());

        setAction.Invoke(setObject, setValue);

        //setDelegate.DynamicInvoke(packet, value);
    }

    public object GetValue(UserPacket packet, FieldInfo field)
    {
        var getDelegate = m_packetFieldGetters[packet.GetType()][field];

        dynamic getFunction = getDelegate; //Convert.ChangeType(getDelegate, getDelegate.GetType());
        dynamic getObject   = packet;      //Convert.ChangeType(packet, packet.GetType());

        return getFunction.Invoke(getObject);

        //return getDelegate.DynamicInvoke(packet);
    }

    public void InitializePackets(Assembly packetsAssembly)
    {
        var typesArray = packetsAssembly.GetTypes();

        foreach (Type type in typesArray)
        {
            if (type.BaseType == typeof(UserPacket))
            {
                InsertPacketConstructor(type);
                InsertSettersAndGetters(type);
            }
        }
    }

    private void InsertPacketConstructor(Type packetType)
    {
        foreach (var member in packetType.GetFields())
        {
           Console.WriteLine(member);
           // TODO: Implement
        }
    }

    private void InsertSettersAndGetters(Type type)
    {
        Dictionary<FieldInfo, Delegate> getters = new Dictionary<FieldInfo, Delegate>();
        Dictionary<FieldInfo, Delegate> setters = new Dictionary<FieldInfo, Delegate>();

        foreach (FieldInfo field in type.GetFields())
        {
            Delegate getDelegate = CreateGetter(type, field.FieldType, field);
            Delegate setDelegate = CreateSetter(type, field.FieldType, field);

            getters.Add(field, getDelegate);
            setters.Add(field, setDelegate);
        }

        m_packetFieldGetters.Add(type, getters);
        m_packetFieldSetters.Add(type, setters);
    }

    private Delegate CreateGetter(Type classType, Type getReturnType, FieldInfo field)
    {
        string methodName = field.ReflectedType.FullName + ".get_" + field.Name;

        Type[] parameterTypes      = new Type[1] { classType };
        DynamicMethod getterMethod = new DynamicMethod(methodName, getReturnType, parameterTypes, true);
        ILGenerator gen = getterMethod.GetILGenerator();

        if (field.IsStatic)
        {
            gen.Emit(OpCodes.Ldsfld, field);
        }
        else
        {
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldfld, field);
        }
        gen.Emit(OpCodes.Ret);

        // Create the specific Func<,> instance
        Type[] typeArgs    = new Type[] { classType, getReturnType };
        Type   generic     = typeof(Func<,>);
        Type   genInstance = generic.MakeGenericType(typeArgs);

        Delegate getterDelegate = getterMethod.CreateDelegate(genInstance);

        return getterDelegate;
    }
    private Delegate CreateSetter(Type classType, Type setValueType,  FieldInfo field)
    {
        string methodName = field.ReflectedType.FullName + ".set_" + field.Name;
        Type[] parameters = new Type[2]
        {
            classType,
            setValueType
        };
        DynamicMethod setterMethod = new DynamicMethod(methodName, null, parameters);
        ILGenerator gen = setterMethod.GetILGenerator();

        if (field.IsStatic)
        {
            gen.Emit(OpCodes.Ldarg_1);
            gen.Emit(OpCodes.Stsfld, field);
        }
        else
        {
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldarg_1);
            gen.Emit(OpCodes.Stfld, field);
        }
        gen.Emit(OpCodes.Ret);

        // Create the specific Action<,> instance
        Type[] typeArgs    = new Type[] { classType, setValueType };
        Type   generic     = typeof(Action<,>);
        Type   genInstance = generic.MakeGenericType(typeArgs);

        Delegate ret = setterMethod.CreateDelegate(genInstance);
        return ret;
        }


    }
}

// THIS IS IN A DIFERENT ASSEMBLY
namespace MirrorNetTesting

{
// This is just an example packet
public class StudentPacket : UserPacket
{
    public int    age;
    public int    height;
    public double grades;
    public string firstName;
    public string lastName;
}
class Program
{
    static void Main(string[] args)
    {
        Assembly asm = Assembly.GetAssembly(typeof(StudentPacket));
        MirrorNetManager.Instance.InitializePackets(asm);

        PerformanceTesting();

        Console.ReadLine();
    }

    public static void PerformanceTesting()
    {
        int studentsCount = 1000 * 100;
        StudentPacket[] studentsArray = new StudentPacket[studentsCount];

        //////////////////////////////////////////////////////////////////////////

        Random rnd = new Random();

        for (int i = 0; i < studentsArray.Length; i++)
        {
            StudentPacket student = new StudentPacket();

            student.age    = rnd.Next();
            student.height = rnd.Next();
            student.grades = rnd.NextDouble();
            student.firstName = "First " + rnd.Next().ToString();
            student.lastName  = "Last "  + rnd.Next().ToString();

            studentsArray[i] = student;
        }

        var fieldsArray = typeof(StudentPacket).GetFields().ToArray();

        //////////////////////////////////////////////////////////////////////////

        // Begin normal getter test
        Console.WriteLine("Testing normal getters");
        Stopwatch normalGetterSw = new Stopwatch();

        normalGetterSw.Start();

        foreach (var student in studentsArray)
        {
            //object getValue;

            var getAge       = student.age;
            var getHeight    = student.height;
            var getGrades    = student.grades;
            var getFirstName = student.firstName;
            var getLastName  = student.lastName;
        }

        normalGetterSw.Stop();

        //////////////////////////////////////////////////////////////////////////

        // Begin reflection getter test
        Console.WriteLine("Testing reflection getters");
        Stopwatch reflectionGetterSw = new Stopwatch();

        reflectionGetterSw.Start();

        foreach (var student in studentsArray)
        {
            object getValue;

            for (int i = 0; i < fieldsArray.Length; i++ )
            {
                FieldInfo field = fieldsArray[i];
                getValue = MirrorNetManager.Instance.GetValue(student, field);
            }
        }

        reflectionGetterSw.Stop();

        //////////////////////////////////////////////////////////////////////////

        // Begin normal setter test
        Console.WriteLine("Testing normal setters");
        Stopwatch normalSetterSw = new Stopwatch();

        int    age       = 10;
        int    height    = 12;
        double grades    = 1432.523d;
        string firstName = "first name";
        string lastName  = "last name";

        normalSetterSw.Start();

        foreach (var student in studentsArray)
        {
            student.age       = age;
            student.height    = height;
            student.grades    = grades;
            student.firstName = firstName;
            student.lastName  = lastName;
        }

        normalSetterSw.Stop();

        //////////////////////////////////////////////////////////////////////////

        // Begin reflection setter test
        Console.WriteLine("Testing reflection setters ");
        Stopwatch reflectionSetterSw = new Stopwatch();

        object[] setValues = new object[]
        {
            age,
            height,
            grades,
            firstName,
            lastName
        };

        reflectionSetterSw.Start();

        foreach (var student in studentsArray)
        {
            for (int i = 0; i < fieldsArray.Length; i++ )
            {
                FieldInfo field = fieldsArray[i];
                MirrorNetManager.Instance.SetValue(student, field, setValues[i]);
            }
        }

        reflectionSetterSw.Stop();

        //////////////////////////////////////////////////////////////////////////

        Console.WriteLine("Normal getter:     \t {0}",     normalGetterSw.ElapsedMilliseconds);
        Console.WriteLine("Normal setter:     \t {0}",     normalSetterSw.ElapsedMilliseconds);
        Console.WriteLine("Reflection getter: \t {0}", reflectionGetterSw.ElapsedMilliseconds);
        Console.WriteLine("Reflection setter: \t {0}", reflectionSetterSw.ElapsedMilliseconds);

        //////////////////////////////////////////////////////////////////////////
    }
}

}

Output (relevant stuff only):

Normal getter:           3
Normal setter:           4
Reflection getter:       261
Reflection setter:       183

Reflection getter and setter are actually the dynamic calls and they usually end up taking 300ms.

Also since the code is pretty long I posted it here also.


Solution

  • Have you considered a slightly different approach? As a main purpose of getters and setters in this case is serialization and deserialization, maybe you should concentrate on dynamically generating serialization methods, which would remove overhead of accessing those fields one by one with use of dynamic methods.

    For example, let's suppose you want to use BinaryFormatter for serialization (although you'll probably choose something better), a goal would be to dynamically generate method like:

        static private byte[] SerializeStudentPacket(StudentPacket packet)
        {
            var bf = new BinaryFormatter();
            var ms = new MemoryStream();
            bf.Serialize(ms, packet.age);
            bf.Serialize(ms, packet.firstName);
            bf.Serialize(ms, packet.grades);
            bf.Serialize(ms, packet.height);
            bf.Serialize(ms, packet.lastName);
            return ms.ToArray();
        }
    

    Which can be made simpler than by ILGenerator by using Linq Expressions:

        ParameterExpression @object = Expression.Parameter(typeof(StudentPacket), "@object");
        MethodInfo serializeMethodInfo = typeof(BinaryFormatter).GetMethod("Serialize", new Type[] { typeof(Stream), typeof(object) });
        MethodInfo toArrayMethodInfo = typeof(MemoryStream).GetMethod("ToArray");
        var bf = Expression.Variable(typeof(BinaryFormatter), "bf");
        var ms = Expression.Variable(typeof(System.IO.MemoryStream), "ms");
        List<Expression> expressions = new List<Expression>();
        expressions.Add(
            Expression.Assign(bf, Expression.New(typeof(BinaryFormatter))));
        expressions.Add(
            Expression.Assign(ms, Expression.New(typeof(MemoryStream))));
        foreach (FieldInfo field in typeof(StudentPacket).GetFields())
        {
            expressions.Add(
               Expression.Call(bf, serializeMethodInfo, ms, 
                               Expression.Convert(Expression.Field(@object, field.Name),
                                                  typeof(object))));
        }
        expressions.Add(Expression.Call(ms, toArrayMethodInfo));
        var lambda = Expression.Lambda(
            Expression.Block(
                new[] { bf, ms },
                expressions
            ),
            @object);
    

    Then you can of course store a result of lambda.Compile() to serialize StudentPacket. The same approach could be also used for deserialization.