Back to Home

NFX - Ultra Efficient Binary Serialization in the CLR

C # · CLR · Serialization · Unistack · .net · NFX · AumCluster · cluster

NFX - Ultra Efficient Binary Serialization in the CLR

    Requirements


    In this article, we will consider the tasks of transferring complex objects between processes and machines. There were many places in our systems where it was required to move a large number of business objects of various structures, for example:

    • self-looping object graphs (trees with back-references)
    • arrays of structures (value types)
    • classes / structures with readonly fields
    • instances of existing .Net collections (Dictionary, List) that internally use custom serialization
    • a large number of instance types specialized for a specific task


    We will talk about three aspects that are very important in distributed cluster systems:

    • serialization / deserialization speed
    • volume of objects in serialized form
    • the ability to use existing objects without the need to “decorate” these objects and their fields with auxiliary attributes for serialization


    Briefly consider the three above aspects.

    The first is speed. This is very important to ensure the overall system performance in a distributed environment, when to complete a task (for example, a request from one user) it is necessary to execute five to ten requests to other back-end machines.

    The second is volume. When transferring / replicating a large amount of data, the budget of the communication channel between the data centers should not be “bloated”.

    The third is convenience.It is very inconvenient when serialization / marshaling only requires the creation of "extra" objects, ctr. transfer data. It is also inconvenient to force a programmer of a particular business type to write low-level code to write an instance to an array of bytes. This can be done when you have 5-6 classes, but what if your system has 30 basic generic classes (i.e. DeliveryStrategy), each of which is combined with dozens of other classes (this gives hundreds of specific types, i.e.: DeliveryStrategy, DeliveryStrategy, DeliveryStrategy etc.). I would very much like to have a transparent system that can serialize almost all classes of the subject area without the need for additional markup, code, etc. Of course, there are things that do not need to be serialized, for example, some unmanaged resources or delegates, but everything else is usually needed,

    This article covers the topic of binary serialization. We will not talk about JSON and other formats, as they are not intended to effectively solve the above problems.

    Issues with Existing Serializers


    Let’s make a reservation right away, everything that is written here is relative - depending on what to compare with. If you write / read hundreds of objects per second, then there is no problem. Another thing is when you need to process tens or even hundreds of thousands of objects per second.

    BinaryFormatter is a .Net veteran. It is easy to use and suits the requirements better than DataContractSerializer. It well supports all built-in collection types and other BCL classes. Supports versioning of objects. Not interoperable between platforms. It has very large disadvantages associated with performance. It is very slow and serialization produces very massive threads.

    DataContractSerializer - WCF engine. Works faster than BinaryFormatter in many cases. Supports interoperability and versioning. However, this serializer is not intended to solve general-purpose serialization problems as such. It requires specialized decoration of classes and fields with attributes; there are also problems with polymorphism and support for complex types. This is very explainable. The fact is that the DataContractSerializer is not intended by definition to work with arbitrary types (hence the name).

    Protobuf - super speed! It uses the Google format, allows you to change the version of objects and superfast. Interoperable between platforms. It has a major significant drawback - it does not “understand” all types automatically and does not support complex graphs.

    Thrift is a Facebook development. Uses its IDL, is interoperable between languages, allows you to change the version. Disadvantages: it works quite slowly, consumes a lot of memory, does not support cyclic graphs.

    Based on the above characteristics, if you do not take into account performance, the most suitable serializer for us is BinaryFormatter. He is the most “transparent." The fact that it does not support interoperability between platforms is not important for us, because we have one platform - Unistack. But the speed of his work is just awful. Very slow and large output.

    NFX.Serialization.Slim.SlimSerializer


    github.com/aumcode/nfx/blob/master/Source/NFX/Serialization/Slim/SlimSerializer.cs

    SlimSerializer is a hybrid serializer with dynamic generation of ser / deser code in runtime for each specific type.

    We did not try to make an absolutely universal decision, because then we would have to sacrifice something. We did not do things that. it doesn’t matter to us, namely:
    • cross platform
    • object version upgrade


    Based on the foregoing, SlimSerializer is not suitable for such tasks where:
    • data is stored in storage (for example, on disk)
    • data is generated / accepted by processes not on the CLR platform, however Windows.NET - to - Linux.MONO and Linux.MONO - to - Windows.NET work great


    SlimSerializer is designed for situations where:
    • need high speed (hundreds of thousands of operations per second)
    • need to save the amount of data transferred
    • specialized markup for serialization is unrealistic for various reasons (for example, a lot of classes)


    SlimSerializer supports all kinds of edge-cases, for example:
    • direct serialization of primitive structures and their Nullable equivalents (DateTime, Timespan, Amount, GDID, FID, GUID, MethodSpec, TypeSpec etc.)
    • direct serialization of basic reference types (byte [], char [], string [])
    • support for classes and structures with read-only fields
    • support for custom-serialization ISerializable, OnSerializing, OnSerialized ... etc.
    • cascading nested serialization (for example, some type does custom-serialization of itself and should call SlimSerializer for some field)
    • allows you to serialize any supported types (except delegates) to the root
    • normalizes graphs of any complexity and nesting
    • buffer-overflow detection in deserialization (this is necessary when the stream is corrected and the unintentional allocation of a large piece of memory is possible)


    The development is not easy and has already undergone many optimizations. The results that we managed to achieve are not final, you can still accelerate, but this will cause complication of the already non-trivial code.

    How it works?

    SlimSeralizer uses a streamer that is taken from the injectable format github.com/aumcode/nfx/blob/master/Source/NFX/IO/StreamerFormats.cs. Streamer formats are needed in order to serialize certain types directly to the stream. For example, we by default support such types as FID, GUID, GDID, MetaHandle etc. The fact is that certain types can be cunningly packaged by variable-bit encoding. This gives a very large increase in speed and saves space. All integer primitives are written with variable-bit encoding. Thus, in cases where you need superfast support of a special type, you can inherit StreamerFormat and add WriteX / ReadX methods. The system itself collects and turns them into lambda functors, which are needed for fast serialization / deserialization.

    TypeDescriptor github.com/aumcode/nfx/blob/master/Source/NFX/Serialization/Slim/TypeSchema.cs is built for each type.. which dynamically compiles a pair of functors for serialization and deserialization.

    SlimSerializer is built on the idea of ​​TypeRegistry and this is the main highlight of the entire serializer github.com/aumcode/nfx/blob/master/Source/NFX/Serialization/Slim/TypeRegistry.cs . Types are written as a string - the full name of the type, but if such a type has already been encountered before, then type handle of the form “$ 123” is written. This denotes the type located in the registry number 123.

    When we meet reference, we replace it with MetaHandle github.com/aumcode/nfx/blob/master/Source/NFX/IO/MetaHandle.cs, which effectively inlines either a string, if reference is to string, or integer, which is the instance number of the object in the object graph, i.e. a kind of pseudo-pointer-handle. With deserialization, everything is reconstructed in the reverse order.

    Performance


    All of the tests below were performed on Intel Core I7 3.2 GHz on a single thread.
    Performance SlimSerializer scales in proportion to the number of threads. We use specialized thread-static optimizations so as not to copy the buffers.

    Take the next type as the “experimental”. Pay attention to the various attributes that are needed for the DataContractSerializer:

    [DataContract(IsReference=true)]
    [Serializable]
    public class Perzon
    {
          [DataMember]public string FirstName;
          [DataMember]public string MiddleName;
          [DataMember]public string LastName;
          [DataMember]public Perzon Parent;
          [DataMember]public int Age1;
          [DataMember]public int Age2;
          [DataMember]public int? Age3;
          [DataMember]public int? Age4;
          [DataMember]public double Salary1;
          [DataMember]public double? Salary2;
          [DataMember]public Guid ID1;
          [DataMember]public Guid? ID2;
          [DataMember]public Guid? ID3;
          [DataMember]public List Names1; 
          [DataMember]public List Names2;        
          [DataMember]public int O1 = 1;
          [DataMember]public bool O2 = true;
          [DataMember]public DateTime O3 = App.LocalizedTime;
          [DataMember]public TimeSpan O4 = TimeSpan.FromHours(12);
          [DataMember]public decimal O5 = 123.23M;
    }
    


    And now we do many times 500,000 objects:
    • Slim serialize: 464 252 ops / sec; size: 94 bytes
    • Slim deser: 331 564 ops / sec
    • BinFormatter serialize: 34 702 ops / sec: size: 1188 bytes
    • BinFormatter deser: 42 702 ops / sec
    • DataContract serialize: 108 932 ops / sec: size: 773 bytes
    • DataContract deser: 41 985 ops / sec


    Slim to BinFormatter serialization speed: 13.37 times faster.
    Slim to BinFormatter deserialization speed: 7.76 times faster.
    Slim to BinFormatter volume: 12.63 times less.

    Slim to DataContract serialization speed: 4.26 times faster.
    Slim to DataContract deserialization speed: 7.89 times faster.
    Slim to DataContract volume: 8.22 times smaller.

    And now we are trying a complex object graph of several dozen mutually referencing objects, including arrays and sheets (many times over 50,000 objects):
    • Slim serialize: 12,036 ops / sec; size: 4 466 bytes
    • Slim deser: 11 322 ops / sec
    • BinFormatter serialize: 2,055 ops / sec: size: 7,393 bytes
    • BinFormatter deser: 2,277 ops / sec
    • DataContract serialize: 3 943 ops / sec: size: 20 246 bytes
    • DataContract deser: 1,510 ops / sec


    Slim to BinFormatter serialization speed: 5.85 times faster.
    Slim to BinFormatter deserialization speed: 4.97 times faster.
    Slim to BinFormatter volume: 1.65 times smaller.

    Slim to DataContract serialization speed: 3.05 times faster.
    Slim to DataContract deserialization speed: 7.49 times faster.
    Slim to DataContract volume: 4.53 times smaller.

    Note the difference when serializing a typed class (the first case is “Perzon”) and the second (many objects). In the second case, there is a complex graph with cyclic relationships between objects, and therefore Slim begins to approach (slow down) in speed to Microsoft. However, it still exceeds the last minimum by 4 times in speed and a half times in volume. Code for this test:github.com/aumcode/nfx/blob/master/Source/Testing/Manual/WinFormsTest/SerializerForm2.cs#L51-104

    And here is a comparison with Apache.Thrift: blog.aumcode.com/2015/03/apache-thrift- vs-nfxglue-benchmark.html .
    Although these numbers are not for pure serialization, but for the whole NFX.Glue (which includes messaging, TCP networking, security etc), the speed very much depends on the SlimSerializer, on which the “native” NFX.Glue bindings are built.

    Each test is:
       64,000 calls each returning a set of 10 rows each having 10 fields
      640,000 total rows pumped
    Glue:     took  1982 msec @ 32290 calls/sec
    Thrift1:  took 65299 msec @   980 calls/sec  32x slower than Glue
    Thrift2:  took 44925 msec @  1424 calls/sec  22x slower than Glue
    =================================================================
    Glue is:
       32 times faster than Thrift BinaryProtocol
       22 times faster than Thrift CompactProtocol
    


    Summary


    The NFX SlimSerializer delivers exceptionally high and predictably robust performance, saving processor and memory resources. This is what opens up opportunities for high-load technologies on the CLR platform, allowing you to process hundreds of thousands of requests per second on each node of distributed systems.

    SlimSerializer has several limitations due to the inability to create a practical “one size fits all” system. These limitations include the lack of versioned data structures, serialization of delegates, and interoperability with platforms other than the CLR. However, it is worth noting that in the Unistack concept (a unified software stack for all system nodes), these restrictions are generally invisible except for the lack of versioning, i.e. SlimSerializer is not designed for long-term storage of data on disk, if the data structure can change.

    The ultra-efficient NFX.Glue native binders allow you to serve 100 thousand + two-way calls per second thanks to specialized optimizations used in the serializer, without requiring the programmer to work extra to create extra data-transfer types

    youtu.be/m5zckEbXAaA

    youtu.be.com/KyhYwaxg2xc


    SlimSerializer significantly outperforms the tools built into .NET, allowing you to efficiently process complex graphs of interrelated objects (which neither Protobuf nor Thrift can do).

    Read Next