Home > OS >  Compiling methods for optimal runtime performance
Compiling methods for optimal runtime performance

Time:05-12

I'm trying to determine what the best way to get fully optimized delegates for various objects to improve the speed of serialization. Simply put: I'd like to remove various different checks, and compile more efficient serialize functions one time at the start of my app.

Let's take a look at this simple example:

public class GamePacket
{
    [Length(10)]
    [ReadBackwards]
    public string Id { get; set; }
}

Now, I'd likely create a serializer, and for performance reasons store the attributes in a cached field. Everytime I want to deserialize a GamePacket from a stream (or byte array), I'd call something like:

Deserialize(byte[] stream)
{
    var header = stream.ReadByte();
    var packet = cachedDeserializers[header];

    var instance = packet.DelegateForCreateInstance();

    foreach (var field in packet.Fields)
    {
        if (field.Type != TypeCode.String) continue;

        var str = stream.ReadBytes(field.LengthAttribute.Length);

        if (field.HasReadBackwardsAttribute)
            str = str.Reverse();

        field.DelegateForSetValue(instance, str);
    }
     
}

The problem now lies in the fact that EVERY time I'm calling Deserialize on that stream, I need to loop through and check various things like attributes, and other checks. In the example, these things can potentially be omitted (And maybe more):

  • if (field.Type != TypeCode.String) continue;
  • if (field.HasReadBackwardsAttribute)

If I know the field has a read backwards attribute, I'd like to compile a simplified delegate on app start that omits these checks, and simply reads it backwards. Is it possible to create a delegate that can remove unneeded logic? For example:

Deserialize(byte[] stream)
{
    var header = stream.ReadByte();
    var packet = cachedDeserializers[header];

    var instance = packet.CallCachedCompile(stream);
}

// CallCachedCompile for GamePacket would look something like this:
CallCachedCompile(byte[] stream)
{
    var instance = this.DelegateForCreateInstance();
    var str = stream.ReadBytes(10);
    str = str.Reverse();
    this.DelegateForSetValue(instance, "Id", str);
    return instance;
}

I've looked briefly into expression trees. Would something like this be doable in expression Trees? What would be the most efficient way?

CodePudding user response:

Yes, using code generation approach you can generate delegates for the particular type. So instead of this generic reflection-like code:

foreach (var field in packet.Fields)
{
    if (field.Type != TypeCode.String) continue;

    var str = stream.ReadBytes(field.LengthAttribute.Length);

    if (field.HasReadBackwardsAttribute)
        str = str.Reverse();

    field.DelegateForSetValue(instance, str);
}

You can generate code for a specific type:

gamePacketInstance.Id = SomeConvertionToString(stream.ReadBytes(field.LengthAttribute.Length).Revers());

The code generation topic is quite big and I don't know what exactly you do inside your delegates. You can generate specific delegates in runtime (emit il or expression trees) or in compile time (source generators). I suggest you to read my article Dotnet code generation overview by example. It will give good overview with examples.

CodePudding user response:

Without using any code generation you can still do this pretty efficiently.

You basically need to use generics and polymorphism to cache all code that you want done for each type that you encounter.

Bearing in mind that properties have underlying methods, we can create delegates to set properties without using code generation.

abstract class DeserializerBase
{
    object DeserializePacket(Stream stream);
}

class Deserializer<T> : DeserializerBase where T : new()
{
    FieldAction<T>[] fieldActions =
        typeof(T).GetProperties()
        .Where(p => p.Type == TypeCode.String)
        .Select(p => IsReverseAttribute(p)
                     ? new FieldActionReverse<T>
                     {
                         Setter = p.SetMethod.CreateDelegate<Action<T, string>>(),
                         Length = GetLengthAttribute(p),
                     }
                     : new FieldAction<T>
                     {
                         Setter = p.SetMethod.CreateDelegate<Action<T, string>>(),
                         Length = GetLengthAttribute(p),
                     })
        .ToArray();

    object DeserializePacket(Stream stream);
    {
        var packet = new T();
        foreach (var action in fieldActions)
            action.Deserialize(packet);
    }
}

class FieldAction<T>
{
    public Action<T, string> Setter;
    public int Length;

    void Deserialize(Stream stream, T instance)
    {
        var str = ReadString(stream);
        Setter(instance, str);
    }

    virtual string GetString(Stream stream)
    {
        return stream.ReadBytes(Length);
    }
}

class FieldActionReverse<T> : FieldAction<T>
{
    override string GetString(Stream stream)
    {
        return stream.ReadBytes(Length).Reverse();
    }
}

Your final entry code becomes this.

Dictionary<int, DeserializerBase> cachedDeserializers = new Dictionary<int, DeserializerBase>
    {
        {5, new Deserializer<GamePacket>()}
    };

Deserialize(Stream stream)
{
    var header = stream.ReadByte();
    var packet = cachedDeserializers[header].DeserializePacket(stream);
}

You can even place generic constraints on T to ensure it is a Packet then you can return a base Packet type from this entry function.

  • Related