Java serialization: maximum speed without a rigid data structure

Our team at Sberbank is developing a session data service that organizes the interchange of a single Java session context between distributed applications. Our service urgently needs very fast serialization of Java objects, as this is part of our mission critical task. Initially, they came to our mind: Google Protocol Buffers , Apache Thrift , Apache Avro , CBORet al. The first three of these libraries require serialization of objects to describe the schema of their data. CBOR is so low-level that it can only serialize scalar values ​​and their sets. What we needed was a Java serialization library that “didn’t ask too many questions” and didn’t force manual sorting of serializable objects “into atoms”. We wanted to serialize arbitrary Java objects without knowing practically anything about them, and we wanted to do this as quickly as possible. Therefore, we organized a competition for the available Open Source solutions to the Java serialization problem.

KDPV

Competitors


For the competition, we selected the most popular Java serialization libraries, mainly using the binary format, as well as libraries that have worked well in other Java serialization reviews .
1Java standardJava- « »,  Java- .
2Jackson JSON FasterXML/jackson-databind, Java- JSON-.
3Jackson JSON (with types), , , full qualified Java-. JSON- (, ) .
, JSON...
[
  "ru.sbrf.ufs.dto.PersonDto",
  {
    "firstName":"Ivan",
    "lastName":"Ivanov"
  }
]
...
:
public ObjectMapper createMapper() {
    return new ObjectMapper();
}
:
public ObjectMapper createMapper() {
    return new ObjectMapper()
            .enable(
                    ACCEPT_SINGLE_VALUE_AS_ARRAY,
                    ACCEPT_EMPTY_STRING_AS_NULL_OBJECT,
                    ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT,
                    READ_UNKNOWN_ENUM_VALUES_AS_NULL,
                    UNWRAP_SINGLE_VALUE_ARRAYS
            )
            .disable(
                    FAIL_ON_INVALID_SUBTYPE,
                    FAIL_ON_NULL_FOR_PRIMITIVES,
                    FAIL_ON_IGNORED_PROPERTIES,
                    FAIL_ON_UNKNOWN_PROPERTIES,
                    FAIL_ON_NUMBERS_FOR_ENUMS,
                    FAIL_ON_UNRESOLVED_OBJECT_IDS,
                    WRAP_EXCEPTIONS
            )
            .enable(ALLOW_SINGLE_QUOTES)
            .disable(FAIL_ON_EMPTY_BEANS)
            .enable(MapperFeature.PROPAGATE_TRANSIENT_MARKER)
            .setVisibility(FIELD, ANY)
            .setVisibility(ALL, NONE)
            .enableDefaultTyping(NON_FINAL);  // !
}
4Jackson Smile FasterXML/jackson-dataformats-binary/smile, Jackson-, Java- JSON- – Smile.
5Jackson Smile (with types), , «Jackson JSON (with types)» (full qualified Java- ).
6Bson4Jackson michel-kraemer/bson4jackson, Jackson-, Java- JSON- – BSON.
7Bson4Jackson (with types), , «Jackson JSON (with types)» (full qualified Java- ).
8BSON MongoDb mongodb/mongo-java-driver/bson,   Java- BSON-.
9Kryo EsotericSoftware/kryo,  Java- .
10Kryo (unsafe), , sun.misc.Unsafe /.
...
:
com.esotericsoftware.kryo.io.Input
com.esotericsoftware.kryo.io.Output
:
com.esotericsoftware.kryo.io.UnsafeInput
com.esotericsoftware.kryo.io.UnsafeOutput
11FSTThe RuedigerMoeller / fast-serialization library  that converts Java objects to its own binary format.
12FST (unsafe)The same library as above, but configured to use the sun.misc.Unsafe class to speed up serialization / deserialization.
Features of the library settings ...
:
FSTConfiguration fst = FSTConfiguration.createDefaultConfiguration();
:
FSTConfiguration fst = FSTConfiguration.createUnsafeBinaryConfiguration();
thirteenOne nioThe odnoklassniki / one-nio library  that converts Java objects into its own binary format.
14One Nio (for persist)The same library as above, but configured in such a way as to include detailed meta information about the class of the serializable Java object in the serialization result. This may be in demand during long-term storage byte[](for example, in the database) before deserialization. That is, the goal pursued is the same as that of Jackson JSON (with types).
Features of the library settings ...
:
byte[] bufWithoutSerializers = new byte[bufferSize];
SerializeStream out = new SerializeStream( bufWithoutSerializers );
out.writeObject(object);
// bufWithoutSerializers is the result
:
byte[] bufWithSerializers = new byte[bufferSize];
PersistStream out = new PersistStream( bufWithSerializers );
out.writeObject(object);
bufWithSerializers = out.toByteArray();
// bufWithSerializers is the result

object- result  -:
1) full qualified object,
2) ,
3) full qualified ,
4) .
- , One Nio , .
Here we go!

Race


Speed ​​is the main criterion for evaluating Java serialization libraries that are participants in our impromptu competition. In order to objectively evaluate which of the serialization libraries is faster, we took real data from the logs of our system and composed synthetic session data from them of different lengths : from 0 to 1 MB. The format of the data was strings and byte arrays.
Note: Looking ahead, it should be said that winners and losers have already appeared on the sizes of serializable objects from 0 to 10 KB. A further increase in the size of objects to 1 MB did not change the outcome of the competition.
In this regard, for better clarity, the following graphs of the performance of Java serializers are limited by the size of objects of 10 KB.
, :
IntelR CoreTM i7-6700 CPU, 3.4GHz, 8 cores
16 GB
Microsoft Windows 10 (64-bit)
JREIBM J9 VM 1.7.0
: , IBM JRE One Nio ( 13 14). sun.reflect.MagicAccessorImpl private final ( ) , . , IBM JRE  sun.reflect.MagicAccessorImpl, , runtime .

(, Serialization-FAQ, One Nio ), fork ,  sun.reflect.MagicAccessorImpl  .  sun.reflect.MagicAccessorImpl  fork- sun.misc.Unsafe .
In addition, in our fork, the serialization of strings was optimized - strings began to be serialized 30-40% faster when working on IBM JRE.

In this regard, in this publication all the results for the One Nio library were obtained on our own fork, and not on the original library.
Direct measurement of serialization / deserialization speed was performed using Java Microbenchmark Harness (JMH) - a tool from OpenJDK for building and running benchmark-s. For each measurement (one point on the graph), 5 seconds were used to “warm up” the JVM and another 5 seconds for the time measurements themselves, followed by averaging.
UPD:
JMH benchmark code without some details
public class SerializationPerformanceBenchmark {

    @State( Scope.Benchmark )
    public static class Parameters {

        @Param( {
            "Java standard",
            "Jackson default",
            "Jackson system",
            "JacksonSmile default",
            "JacksonSmile system",
            "Bson4Jackson default",
            "Bson4Jackson system",
            "Bson MongoDb",
            "Kryo default",
            "Kryo unsafe",
            "FST default",
            "FST unsafe",
            "One-Nio default",
            "One-Nio for persist"
        } )
        public String serializer;
        public Serializer serializerInstance;

        @Param( { "0", "100", "200", "300", /*... */ "1000000" } )  // Toward 1 MB
        public int sizeOfDto;
        public Object dtoInstance;
        public byte[] serializedDto;

        @Setup( Level.Trial )
        public void setup() throws IOException {
            serializerInstance = Serializers.getMap().get( serializer );
            dtoInstance = DtoFactory.createWorkflowDto( sizeOfDto );
            serializedDto = serializerInstance.serialize( dtoInstance );
        }

        @TearDown( Level.Trial )
        public void tearDown() {
            serializerInstance = null;
            dtoInstance = null;
            serializedDto = null;
        }
    }

    @Benchmark
    public byte[] serialization( Parameters parameters ) throws IOException {
        return parameters.serializerInstance.serialize(
                parameters.dtoInstance );
    }

    @Benchmark
    public Object unserialization( Parameters parameters ) throws IOException, ClassNotFoundException {
        return parameters.serializerInstance.deserialize(
                parameters.serializedDto,
                parameters.dtoInstance.getClass() );
    }
}

Here's what happened: First, we note that library options that add additional meta-data to the serialization result are slower than the default configurations of the same libraries (see the “with types” and “for persist” configurations). In general, regardless of the configuration, Jackson JSON and Bson4Jackson, who are out of the race, become outsiders according to the results of serialization . In addition, Java Standard drops out of the race based on deserialization results , as for any size of serializable data, deserialization is much slower than competitors. Take a closer look at the remaining participants: According to the results of serialization, the FST library is in confident leaders

Racing - all participants







Racing - except for outsiders

, and with an increase in the size of objects, One Nio “steps on her heels” . Note that for One Nio, the “for persist” option is much slower than the default configuration for serialization speed.
If you look at deserialization, we see that One Nio was able to overtake FST with increasing data size . In the latter, on the contrary, the non-standard configuration “unsafe” performs deserialization much faster.

In order to put all the points over AND, let's look at the total result of serialization and deserialization: It became obvious that there are two unequivocal leaders: FST (unsafe) and One Nio . If on small objects FST (unsafe)

Racing - except for outsiders (overall classification)


 confidently leads, then with the increase in the size of serializable objects, he begins to concede and, ultimately, inferior to One Nio .

The third position with the increase in the size of serializable objects is confidently taken by BSON MongoDb , although it is almost two times ahead of the leaders.

Weighing


The size of the serialization result is the second most important criterion for evaluating Java serialization libraries. In a way, the speed of serialization / deserialization depends on the size of the result: it is faster to form and process a compact result than a volume one. To "weight" the results of serialization, all the same Java objects were used, formed from real data taken from the system logs (strings and byte arrays).

In addition, an important property of the result of serialization is also how much it compresses well (for example, to save it in the database or other storages). In our competition, we used Deflate compression algorithm , which is the basis for ZIP and gzip.

The results of the "weighing" were as follows:

Weighing

It is expected that the most compact results were serialization from one of the leaders of the race: One Nio .
The second place in compactness went to BSON MongoDb  (which took third place in the race).
In third place in terms of compactness, the Kryo library “escaped” , which had previously failed to prove itself in the race.

The serialization results of these 3 leaders of "weighing" are also perfectly compressed (almost two). It turned out to be the most uncompressible: the binary equivalent of JSON is Smile and JSON itself.

A curious fact - all the winners of the "weighing" during serialization add the same amount of service data to both small and large serializable objects.

Flexibility


Before making a responsible decision about choosing a winner, we decided to thoroughly check the flexibility of each serializer and its usability.
For this, we compiled 20 criteria for evaluating our serializers participating in the competition so that “not a single mouse would slip” past our eyes.

Flexibility
Footnotes with Explanations
1    LinkedHashMap.
2    — , — .
3    — , — .
4    sun.reflect.MagicAccessorImpl — : boxing/unboxing, BigInteger/BigDecimal/String. MagicAccessorImpl ( ' fork One Nio) — .
5    ArrayList.
6    ArrayList HashSet .
7    HashMap.
8    — , , /Map-, ( HashMap).
9    -.
10  One Nio — , ' fork- — .
11 .
UPD: According to the 13th criterion, One Nio (for persist) received another point (19th).

This meticulous “examination of applicants” was perhaps the most time-consuming stage of our “casting”. But then these comparison results open well the convenience of using serialization libraries. As a consequence, you can use these results as a reference.

It was a shame to realize, but our leaders according to the results of races and weighing - FST (unsafe) and One Nio- turned out to be outsiders in terms of flexibility ... However, we were interested in a curious fact: One Nio in the “for persist” configuration (not the fastest and not the most compact) scored the most points in terms of flexibility - 19/20. The opportunity to make the default (fast and compact) One Nio configuration work as flexibly also looked very attractive - and there was a way.

At the very beginning, when we introduced the participants to the competition, it was said that One Nio (for persist) included in the result of serialization detailed meta-information about the class of the serializable Java object(*). Using this meta information for deserialization, the One Nio library knows exactly what the class of the serializable object looked like at the time of serialization. It is on the basis of this knowledge that the One Nio deserialization algorithm is so flexible that it provides the maximum compatibility resulting from serialization byte[].

It turned out that meta-information (*) can be separately obtained for the specified class, serialized to  byte[] and sent to the side where the Java objects of this class will be deserialized:
With code in steps ...
//  №1:  -   SomeDto
one.nio.serial.Serializer<SomeDto> dtoSerializerWithMeta = Repository.get( SomeDto.class );
byte[] dtoMeta = serializeByDefaultOneNioAlgorithm( dtoSerializerWithMeta );
//  №1:  dtoMeta  №2

//  №2:  -    SomeDto      One Nio
one.nio.serial.Serializer<SomeDto> dtoSerializerWithMeta = deserializeByOneNio( dtoMeta );
Repository.provideSerializer( dtoSerializerWithMeta );

//  №1:    SomeDto
byte[] bytes1 = serializeByDefaultOneNioAlgorithm( object1 );
byte[] bytes2 = serializeByDefaultOneNioAlgorithm( object2 );
...
//  №1:    №2

//  №2:      SomeDto
SomeDto object1 = deserializeByOneNio( bytes1 );
SomeDto object2 = deserializeByOneNio( bytes2 );
...

If you perform this explicit procedure for exchanging meta-information about classes between distributed services, then such services will be able to send serialized Java objects to each other using the default (fast and compact) One Nio configuration. After all, while the services are running, the versions of the classes on their sides are unchanged, which means there is no need to “drag back and forth” the constant meta information within each serialization result during each interaction. Thus, having done a little more action in the beginning, then you can use the speed and compactness of One Nio simultaneously with the flexibility of One Nio (for persist) . Exactly what is needed!

As a result, for transferring Java objects between distributed services in serialized form (this is what we organized this competition for) One Nio was the winner in flexibility  (19/20).
Among the Java serializers that distinguished themselves earlier in racing and weighing, not bad flexibility was demonstrated:

  • BSON MongoDb  (14.5 / 20),
  • Kryo (13/20).

Pedestal


Recall the results of past Java serialization competitions:

  • in races, the first two lines of the rating were divided by FST (unsafe) and One Nio , and BSON MongoDb took the third place ,
  • One Nio defeated the weigh-in , followed by BSON MongoDb and Kryo ,
  • in terms of flexibility, just for our task of exchanging session context between distributed applications, One Nio again went first  , and BSON MongoDb and Kryo excelled .

Thus, in terms of the totality of the results achieved, the pedestal we obtained is as follows:

  1. One Nio
    In the main competition - races - shared the first place with FST (unsafe) , but significantly weighed the competitor in weighing and testing flexibility.
  2. FST (unsafe)
    Also a very fast Java serialization library, however, it lacks the direct and backward compatibility of the byte arrays resulting from the serialization.
  3. BSON MongoDB + Kryo
    2 3- Java-, . 2- , . Collection Map, BSON MongoDB custom- / (Externalizable ..).

In Sberbank, in our session data service, we used the One Nio library , which won first place in our competition. Using this library, Java session context data was serialized and transferred between applications. Thanks to this revision, the speed of session transport has accelerated several times. Load testing showed that in scenarios that are close to the actual behavior of users in Sberbank Online, an acceleration of up to 40% was obtained only due to this improvement alone. Such a result means a reduction in the response time of the system to user actions, which increases the degree of satisfaction of our customers.

In the next article I will try to demonstrate in action the additional acceleration of One Nioderived from using the class sun.reflect.MagicAccessorImpl. Unfortunately, the IBM JRE does not support the most important properties of this class, which means that the full potential of One Nio on this version of the JRE has not yet been revealed. To be continued.

All Articles