Skip to content

z_archived documentation for fst 1.x

Antony Kellermann edited this page Dec 3, 2020 · 3 revisions

Archived in case one decides to use 1.x (slightly faster)

Serialization

Plain ObjectOutputStream Replacement

Simple Source Example

Basically you just replace ObjectOutputStream, ObjectInputStream with FSTObjectOutput,FSTObjectInput.

public MyClass myreadMethod( InputStream stream ) throws IOException, ClassNotFoundException 
{
    FSTObjectInput in = new FSTObjectInput(stream);
    MyClass result = (MyClass)in.readObject();
    in.close(); // required !
    return result;
}

public void mywriteMethod( OutputStream stream, MyClass toWrite ) throws IOException 
{
    FSTObjectOutput out = new FSTObjectOutput(stream);
    out.writeObject( toWrite );
    out.close(); // required !
}

if you know the type of the Object (saves some bytes for the class name of the initial Object) you can do:

public MyClass myreadMethod(InputStream stream) throws IOException, ClassNotFoundException
{
    FSTObjectInput in = new FSTObjectInput(stream);
    MyClass result = in.readObject(MyClass.class);
    in.close();
    return result;
}

public void mywriteMethod( OutputStream stream, MyClass toWrite ) throws IOException 
{
    FSTObjectOutput out = new FSTObjectOutput(stream);
    out.writeObject( toWrite, MyClass.class );
    out.close();
}

Note
if you write with a type, you also have to read with the same type.

Note
if you create an instance with each serialization you should close the FSTStream, because behind the scenes some datastructures are cached and reused. If this fails, you might observe a performance hit (too much object creation going on), especially if you encode lots of smallish objects.

Recommended threadsafe Use

In order to optimize object reuse and thread safety, FSTConfiguration provides 2 simple factory methods to obtain input/outputstream instances:

...
// ! reuse this Object, it caches metadata. Performance degrades massively
// if you create a new Configuration Object with each serialization !
static FSTConfiguration conf = FSTConfiguration.createDefaultConfiguration();
...
public MyClass myreadMethod(InputStream stream) throws IOException, ClassNotFoundException
{
    FSTObjectInput in = conf.getObjectInput(stream);
    MyClass result = in.readObject(MyClass.class);
    // DON'T: in.close(); here prevents reuse and will result in an exception      
    stream.close();
    return result;
}

public void mywriteMethod( OutputStream stream, MyClass toWrite ) throws IOException 
{
    FSTObjectOutput out = conf.getObjectOutput(stream);
    out.writeObject( toWrite, MyClass.class );
    // DON'T out.close() when using factory method;
    out.flush();
    stream.close();
}

This will create and reuse a single FSTIn/OutputStream instance per thread, which implies you should not save references to streams returned from that method. You can also use a global FSTConfiguration throughout your app using FSTConfiguration.getDefaultConfiguration() (that's what configuration free constructors do)

Note
FSTObjectIn/Output are not threadsafe, only one thread can read/write at a time. FSTConfiguration (holding class metadata) is threadsafe, can be shared applicationwide. It uses a lockfree synchronization to minimize contention.

Accessing underlying byte data

FSTObjectOutput temporary serializes into a byte array. You can set the underlying byte array, access it etc. . This makes it easy i.e. to serialize an Object, then take the resulting byte array and do whatever you want with that. Both streams have a "resetForReuse()" which allows to reuse a given instance (e.g. write an object, take the bytearray and send it somewhere or multiple times without re-encoding, reset and write another object). This way one can avoid massive object creation overhead often induced by closed Stream Interfaces. Ofc you have to be careful regarding accidental shared byte[] arrays when doing so.

What is that FSTConfiguration ?

This class defines the encoders/decoders used during serialization. Usually you just create one global singleton (instantiation of this class is very expensive). Usage of several distinct Configurations is for special use cases which require some in-depth knowledge of FST code. You probably never will need more than this one default instance.

e.g.

public class MyApplication {
    static FSTConfiguration singletonConf = FSTConfiguration.createDefaultConfiguration();
    public static FSTConfiguration getInstance() {
        return singletonConf;
    }
}

You can customize the FSTConfiguration returned by createDefaultConfiguration(). E.g. register new or different serializers, some hooks, set additional flags on defined serializers etc. . Just have a look on the source.

Ressolvement Order

  • If a serializer is registered, this will be used
  • Check for externalizable interface
  • Check for JDK special methods (e.g. writeObject/readObject/readReplace/writeReplace). If found, use compatibility mode for that class (=>slow, avoid)
  • Use default FST serialization implementation

####Pregistering Classes

One easy and important optimization is to register classes which are serialized for sure in your application at the FSTCOnfiguration object. This way FST can avoid writing classnames.

        final FSTConfiguration conf = FSTConfiguration.createDefaultConfiguration();
        conf.registerClass(Image.class,Media.class,MediaContent.class,Image.Size.class,Media.Player.class);

Frequently it is no problem figuring out most frequently serialized classes and register them at application startup time. Especially for short messages/small objects the need to write full qualified classnames hampers performance. Anyway fst writes a class name only once per stream.

Note
Reader and writer configuration should be identical. Even the order of class registration matters.

Unshared mode

In case you just want to serialize plain cycle-free message objects as fast as possible, you can make use of the FSTObjectIn/OutputNoShared subclasses. Omitting cycle detection and detection of identical objects allows to cut corners in many places. You might also want to preregister all message classes. Performance difference can be up to 40%.

Note
Set FSTConfiguration.setShareReferences() to false. When mixing shared/unshared mode in a single application, create two instances of FSTConfiguration.

The unshared versions do not support JDK serialization methods such as readReplace/writeObject/readObject. If you need these to be reflected, use regular FSTObjectInput/Output streams having a configuration with setSharedReferences(false). This will still yield a reasonable performance improvement.

Multi threaded read/write

You cannot read write from different Threads into the same FSTInput/OutputStream. However you can create/use an arbitrary number of FSTInput/OutputStreams sharing one FSTConfiguration. Just see above "Recommended Usage"

Huge Objects / chunked, streaming I/O:

The encoded Objects are written to the underlying stream once you close/flush the FSTOutputStream. Vice versa, the FSTInput reads the full stream until it starts decoding.

This may be a problem in case you read/write huge Objects or want to stream an object graph in small chunks.

A work around in the current version would be to write your Objects chunked (e.g. if you have a List of Objects to serialize, create a new FSTObjectOutput for each Object of the list). Usually allocating some 100 kByte (or even MB) of byte arrays should not be a problem, I just mention this limitation in case you plan reading/writing huge object graphs or you are trying to stream an object graph in small chunks.

I know of users still preferring FST for very large object graphs. Maximum size is then determined by int index, so an object graph has a max size of ~1.5 GB.

Conditional Decoding

There are scenarios (e.g. when using multicast), where a receiver conditionally wants to skip decoding parts of a received Object in order to save CPU time. With FST one can achieve that using the @Conditional annotation.

class ConditionalExample {
   int messagetype;

   @Conditional
   BigObject aBigObject;

...
}

if you read the Object, do the following:

        FSTObjectInput.ConditionalCallback conditionalCallback = new FSTObjectInput.ConditionalCallback() {
            @Override
            public boolean shouldSkip(Object halfDecoded, int streamPosition, Field field) {
                return ((ConditionalExample)halfDecoded).messagetype != 13;
            }
        };
        ...
        ...
        FSTObjectInput fstin = new FSTObjectInput(instream, conf);
        fstin .setConditionalCallback(conditionalCallback);
        Object res = in.readObject(cl);

The FSTObjectInput will deserialize all fields of ConditionalExample then call 'shouldSkip' giving in the partially-deserialized Object. If the shouldSkip method returns false, the @Conditional reference will be decoded and set, else it will be skipped.

Custom Serializers

By default FST falls back to the methods defined by the JDK. Especially if private methods like 'writeObject' are involved, performance suffers, because reflection must be used. Additionally the efficiency of some stock JDK classes is cruel regarding size and speed. The FST default configuration already registers some serializers for common classes (popular Collections and some other frequently used classes).

So if you have trouble with stock JDK serilaization speed/efficiency, you might want to register a piece of custom code defining how to read and write an object of a specific class.

the basic interface to define the serialization of an Object is FSTObjectSerializer. However in most cases you'll use a subclass of FSTBasicObjectSerializer.

The FSTDateSerializer delivered with FST (note the registration in the instantiate method, you need to do it if you instantiate the object by yourself):

public class FSTDateSerializer extends FSTBasicObjectSerializer {
    @Override
    public void writeObject(FSTObjectOutput out, Object toWrite, FSTClazzInfo clzInfo, FSTClazzInfo.FSTFieldInfo referencedBy) 
    {
        out.writeFLong(((Date)toWrite).getTime());
    }

    @Override
    public void readObject(FSTObjectInput in, Object toRead, FSTClazzInfo clzInfo, FSTClazzInfo.FSTFieldInfo referencedBy)
    {
    }

    @Override
    public Object instantiate(Class objectClass, FSTObjectInput in, FSTClazzInfo serializationInfo, FSTClazzInfo.FSTFieldInfo referencee, int streamPositioin) 
    {
        Object res = new Date(in.readFLong());
        in.registerObject(res,streamPositioin,serializationInfo);
        return res;
    }
}

a serializer is registered at the FSTConfiguration Object:

   static {
   ...
   conf = FSTConfiguration.createDefaultConfiguration();     
   conf.registerSerializer(Date.class, new FSTDateSerializer(), false);
   ...
   }

(ofc you have to use exactly this configuration later on in the FSTObjectIn/OutputStream).

Note
The reason having 3 methods (read, write, instantiate) allows to read from the stream before creating the object (e.g. to decide which class to create). Common case is to just override and implement read/write, however there are cases where read is empty and the full object is created and read in the instantiate method.

Unsafe / Byte Order Issues

** Note ** Unsafe mode has been removed in 1.5, but might come back in a later, refactored 2.x version of FST. Reason is it paid off for native arrays only, so for the rare cases you encode huge primitive arrays, just use externalize on the specific class for now if you really want to use Unsafe

Note
Enabling unsafe is not worth the risk in most scenarios. However it can be useful in certain high performance communication heavy data processing. Especially primitive arrays and String encoding improves by an order of magnitude. Usual object graphs and structures do not profit from enabling of unsafe a lot.

Setting System property fst.unsafe=true (e.g. -Dfst.unsafe=true on commandline) lets FST make use of unsafe operations to speed up. It has been proven to be reliable across machines, JDK version and operating systems (WinX,Linux) in production systems. It is recommended to use set fst.unsafe to "true" only if you have a very stable application without any class version mismatch issues. Else you might run into trouble/crashes. Consider this as a special option usable for inhouse / intra server communication. Never use in clients as they might be outdated. Version mismatch can cause unpredictable behaviour when unsafe use is turned on.

If FSTConfiguration.preferSpeed is true, also native arrays will be serialized using unsafe operations, which means no value compression is applied and may result in significant higher size of a serialized object. However often speed matters more than size e.g. when serializing to Off-Heap, Shared Memory queues or fast networks such as IB or 10GBit ethernet (even 1GBit ethernet is not that easy saturated if one uses some of the various slowish enterprise frameworks).

Usage of Unsafe can be enabled by calling "System.setProperty("fst.unsafe","true")" prior to referencing any FST class. A better approach is to switch Unsafe usage at command line like java -Dfst.unsafe=true ... when starting your program.

If you use FST in client server applications or heterogenous networks you might run into byte order issues, as the byteorder of an x86 and (RIP) Solaris SPARC machine are different.

In contradiction to standard Java IO, FST always assumes x86 byte order even when Unsafe is turned off, this means you can encode from an x86 server with Unsafe enabled and decode on a Client with another processor architecture as long Unsafe on the Client is disabled.

So on Big Endian platforms, never turn on Unsafe usage wether its a client or server machine. Ofc this does not hold true if ALL machines de/encoding FST Objects are Big Endian.

Clarification: Disable unsafe on all Big Endian Platforms (non-x86), except when all machines (client+server) are Big Endian. You can enable Unsafe always on Little Endian (x86) machines.

#Structs Disclaimer
This library uses unsafe and direct memory access. There are some sanity checks, however you can construct (wrong) code which will let your VM coredump. It would be possible to create a ByteBuffer backed version later on (by implementing a "Bytez" wrapper for ByteBuffer).

You are encouraged to at least read every heading of this document, trial-and-error will not work that good for this library

since version 1.34 structs can optionally be backed by native allocated memory. Since this document was written earlier, it still uses the term "backing byte[]" in some places.

What are FST Structs ?

FST Structs enable storage of structured data in a continuous block of memory. The memory can be allocated on the heap using a byte[] array or can be allocated off the java heap in native memory.

Why FST Structs ?

  • Memory has become cheap, but we cannot make use of large heaps (>32GB) in java, because GC pauses increase with Java-Heap Size. (see also this blogpost and related).
  • Structured Data in Java has high overhead because embedded Datatypes (e.g. String) are full blown objects. A String member of a class with value "A" requires 60 to 80 bytes on the heap (depends on VM version and 32/64 bit), which means >90% memory overhead. Frequently your 60 GB on-heap data structures boil down to some 5GB stored in packed structs.
  • Using FST Structs as a message encoding (e.g. direct inter process messaging in a cluster) one can skip en/decoding completely as it is possible to directly access message data without en-/decoding.
  • When doing high performance mass data processing, structs provide better cache locality which can result in significant better throughput.

Use cases

  • store/cache huge amounts of data records without impact on GC duration
  • highperformance data transfer in a cluster or inbetween processes

Introduction

In C++, one needs to sacrifice performance in order to get convenience, in Java one needs to sacrifice convenience in order to get performance

FSTStructs provide a way to store and access data in a deterministic structured layout in a continuous chunk of memory. I therefore use runtime byte code generation and (may change in future) the Unsafe class in order to redirect member access inside methods to an underlying byte[] array or offheap memory (see Bytez class).

While there are compromises to be made regarding convenience, there are benefits when "flattening" data structures in a still accessible way:

  • low GC cost (store GB of structured data with <1s Full GC duration)
  • (un-)marshalling is equal to a memory copy. This speeds up inter process communication using shared memory or network messages.
  • use memory mapped files to virtually enlarge your memory, as it is possible to control memory layout of your data
  • faster iteration of complex data structures compared to On-Heap due to control over in-memory layout (locality!) and "pointer-alike" memory access patterns
  • data structures can be de/encoded easy from external languages
  • nearby allocation free code for extreme requirements (e.g. latency).

Memory layout:

.

How does this work ?

Structs are stored in byte arrays or native allocated offheap memory. FST generates "pointer"-alike Wrapper Classes at runtime enabling access to the flat objects structures.

To define the actual layout of a struct, a application needs to provide an OnHeap template instance. The template instance defines length of arrays and strings by containing placeholder instances. This is required as like in regular C structs, everything is of fixed length. A template instance can be used to create new struct instances with help of an Allocator instance. The values of the template instance also determine initial values of a newly allocated struct instance. Constructors of a struct class are not executed (except one creates the struct class on-heap using regular "new").

Technically, FST internally separates object header and instance fields by patching bytecode accessing member fields. To transform arrays and substructures, special getters/setters need to be defined to make things work.

In order to access struct data, a wrapper instance pointing to the byte data is created. By moving the base-address-pointer of such an accessor class one can use a single instance to access many instances of byte-stored structs. FST has a per-thread cache of those wrappers (i call them pointer) in order to enable convenient access to structures and embedded substructures.

Code Examples:

        // build template
        TestInstrument template = new TestInstrument();
        template.legs = new TestInstrumentLeg[] { new TestInstrumentLeg(), null, null, null };
        // use template to allocate 'byte-packed' instances
        FSTStructAllocator<TestInstrument> allocator = 
             new FSTStructAllocator<TestInstrument>(template,SIZE);

        StructArray<TestInstrument> instruments = allocator.newArray(100000);

results in a flat array of in-place copies of the given "template" instance. One can access this like usual object structures like

        sum = 0;
        for ( int i = 0; i < instruments.size(); i++ ) {
            sum+=instruments.get(i).getAccumulatedQty();
        }

In order to pass a structure embedded object to outer code, a 'pointer' (accessor class instance) must be created. FST Byte code instrumentation automatically creates and caches those 'accessor classes'. However if you want to 'keep' (assign) such a struct object, a call to 'detach()' is required. Else subsequent calls will point your accessor to another instance (accessors wrappers are reused).

        TestInstrumentLeg toDetach = instruments.get(i).legs(1);
        toDetach.detach();
        otherCode(toDetach);

Embedded Objects can be rewritten, however one gets an exception if the new object requires more bytes than the previous.

Rules for structable classes

  • a structable class has to inherit FSTStruct
  • a struct class must only contain fields that reference subclasses of FSTStruct or primitive types. It is not possible to have references from within a struct to a heap object.
  • no direct public field access possible. You need to use getter/setter methods. This does not apply for member methods.
  • all fields and methods must be non-final, public or protected. (required to enable instrumentation)
  • Valid structs cannot be inner classes of other FSTStruct classes.
  • no direct references to embedded arrays are allowed. You have to create array accessor methods following a defined naming pattern.
  • no support for arrays of arrays (multidimensional) within structs
  • all sizings are determined at instantiation time (by using a template), this also applies to strings. {{{StructStrings}}} are fixed size.
  • there are several method naming schemes which are recognized by FST byte code instrumentation. This way it is possible to e.g. get the base index of an embedded int array in case.
  • if you want to 'keep' a reference to an embedded object (store tmp variable in a loop or field) you need to call detach() on that reference
  • you cannot synchronize on struct objects
  • the template object defining the layout and initial values of struct(s) must not contain cyclic references
  • System.identityHashcode delivers the identity of the access wrapper, not the embedded object.

Although the list above seems pretty long, if you keep things simple and avoid methods besides getters/setters its not that hard to create valid struct classes. Use structs as pure data record storage rather than full fledged objects to avoid trouble.

Simple fields

primitive fields can be declared straight forward and can be accessed directly from method code.

public class SubStruct extends FSTStruct {
    StructString name = new StructString(30);
    int age = 42;
    ... getters / setters ommitted here ...
}

public class Example extends FSTStruct {

    protected int intField;
    protected double d = 66666666666.66;
    protected SubStruct sub = new SubStruct();

    public int getIntField() { return intField; }
    public void setIntField(int val) { intField = val; }

    public SubStruct getSub() { return sub ; }
    public void setSub(SubStruct val) { sub = val; }

    public void multiply(int mul) {
        // direct member access is OK inside your struct class (and subclasses)
        intField *= mul; 
    }

    public double getD() {
        return d;
    }

    public String toString() {
        return "Example if:"+intField+" d:"+d;
    }
}

Note, that this class is fully operable when allocated as usual on the heap.

if we create a struct array from that

    STStructAllocator<Example> allocator = 
             new FSTStructAllocator<Example>(new Example());

    StructArray<Example> exampleArray = allocator.newArray(1000000);

    Example onHeap = new Example();
    exampleArray.get(10).getIntField(); // get volatile pointer to 10't element and get intVal
    exampleArray.get(10).getSub().getName().setString("Me"); // rewrite StructString content
    exampleArray.get(10).getSub().setName( new StructString("You") ); // rewrite StructString object

we get an array of 1 million Example struct instances, initialzed as a copy of the template given by "new Example()". The difference is, that a "normal" implementation would have created 3.000.001 Objects (array and 1 million Example, 1 million substruct, 1 million structstring instances), which (if not temporary) will cost the garbage collector ~800 to 1250 ms to traverse. The struct example actually allocates one large byte array, which will have practical no impact on GC duration. The data is 'hidden' from the Garbage Collector.

Important: calling a setter on an struct's embedded object will copy the given object. When calling a setter on normal object, a reference will be stored. For structs, every set is "by value" not by reference. This is a technical necessity.

if you examine in a debugger the (volatile) structpointer obtained by {{{exampleArray.get(10).getSub()}}} you will note, that the instance variables are null or 0. This is because FST instrumentation patches all accesses of the methods of 'Example' and redirects the read/write to the underlying byte array. This is not the case if you allocate it regular on the heap with {{{new}}}.

Important: If you need a permanent reference, call 'detach()' on the volatile access wrapper:

    SubStruct sub = exampleArray.get(10).getSub();
    sub.detach();

Arrays of Primitives

while it is possible to patch field access in a methods byte code in order to redirect the code from fields to the {{{byte[]}}} backing the struct, this does not work for arrays. Therefore there are harder rules to follow when using arrays of primitives inside a struct's code.

class .. extends FSTStruct {
    protected int array[] = new int[50];
    public int array(int index) { return anArray[index]; }
    public void array(int index, int value) { anArray[index] = value; }
    public int arrayLen() { return anArray.length; }

    public void addToAll( int toAdd ) {
        for ( int i = 0; i < arrayLen(); i++ )
            array(i,array(i)+toAdd);
    }

All array accesses must use the 3 accessor methods. Once this class is struct-allocated, instrumentation will redirect those accessormethods to another place (array will be null then).

Again you can test your code using usual 'new' allocation as its hard to debug the instrumented struct version of your class.

The naming pattern has to be

protected|public int [arrayfieldName]Len()
protected|public int [arrayfieldName](int index)
protected|public void [arrayfieldName](int index, int value)

if these naming pattern is not used, instrumentation will not patch the method resulting in malfunction of your class once it is struct allocated.

With normal onheap allocation, the array will be allocated somewhere on the heap and the reference to this array is stored in the 'array' field of the example given above.

When this class is struct allocated, the array elements will directly sit behind the objects field data. This can actually increase access performance, as the risk of getting a CPU cache miss is much lower. A cache miss requiring the CPU to access main memory can be aequivalent to 300-1000 CPU instructions.

It is possible to obtain the base adress (explained below in hacking section) of the embedded array data and read this direct without the {{{[]}}} operator. This can actually be faster than usual array[index] on heap performance. However this is required in rare cases and the resulting code is very C-ish and strange.

Arrays of Objects

arrays of substructures should be kept of equal types, else things might get complicated. The same rules as for primitive arrays apply, however there is the exception that you somehow try to set a larger Object than initially given by the template, this will result in an exception.

class .. extends FSTStruct {

    protected StructString array[] = { new StructString("x"), new StructString("xx"), new StructString("xxx",10)};

    public StructString array(int index) { return anArray[index]; }
    public void array(int index, StructString value) { anArray[index] = value; }
    public int arrayLen() { return anArray.length; }

Note that the array content has to be initialized in the template instance given to the allocator class. FST will search for the largest element in your template array, this will then determine the size of each element in the struct array. In the example above, all StructStrings of the array will get a max len of 10, because the last element has this size.

One can also use the @Templated annotation to define the initial values of a Struct array.

class .. extends FSTStruct {

    @Templated
    protected StructString array[] = { new StructString("empty",120), null, null,};

    public StructString array(int index) { return anArray[index]; }
    public void array(int index, StructString value) { anArray[index] = value; }
    public int arrayLen() { return anArray.length; }

in this case all elements in the array will be initialized with a copy of the first element.

To define larger array size consider a template setup method like:

class MyStruct extends FSTStruct {

    public static MyStruct createTemplate() {
        MyStruct res = new MyStruct();
        // set template in first entry (see @Templated below)
        res.array[0] = new StructString("initial value");
        return res;
    }

    @Templated
    protected StructString array[] = new StructString[200];

    public StructString array(int index) { return anArray[index]; }
    public void array(int index, StructString value) { anArray[index] = value; }
    public int arrayLen() { return anArray.length; }

untyped Object arrays

it is possible to store losely typed substructures e.g.:

class UnTyped extends FSTStruct {

    protected FSTStruct array[] = { new StructString("x"), new StructInt(13), new SubStruct()};

    public FSTStruct array(int index) { return anArray[index]; }
    public void array(int index, FSTStruct value) { anArray[index] = value; }
    public int arrayLen() { return anArray.length; }

however things get tricky, you have to call cast() then when accessing those elements

for (..) {
   FSTStruct tmp = unTyped.array(i).cast();
   if ( tmp instanceof .. ) {
   }
}

a note on performance: if you set an object (field or array element), the content of the given object is copied in place of the previous object. Its faster, if you create an offheap instance of the object to set ((memCopy). Additionally you need fewer object allocations.

   StructString tmp = allocator.newStruct(new StructString(20));
   for ( int i = 0; .. ) {
      tmp.setString( "count "+i );
      mystruct.array(i,tmp); // copy !
   }

is faster than

   for ( int i = 0; .. ) {
      mystruct.array(i,new StructString("count "+i));
   }

Built in Struct Types

{{{StructString}}} is a mutable String which can be used inside FSTStructs. One could add a lot more methods to this for convenience, however i was too lazy to do this, contributions are welcome.

{{{StructArray}}} is a fixed-size {{{ArrayList}}} alike. Mostly used as top level to allocate arrays of structs.

{{{StructMap}}} is a fixed size open addressed Hashmap implementation (no remove operation) operating completely inside the off heap. It does not like polymorphic key/values so keep key class and value class constant. Can be used to flatten hashmaps. Additionally this is a proof of concept as there is actually some code operating on the structs underlying byte array. It is slightly slower than {{{HashMap}}} when doing micro benchmarks, however in a real application it performs way better as locality (CPU cache) is maintained, while a {{{HashMap}}}'s keys and values are likely cluttered all over the heap (cache misses).

{{{StructInt}}} present to be able to use integer keys in {{{StructMap}}}

FSTStructAllocator and allocation patterns

Let's look on a object on the heap

Usually an object contains several subobjects. E.g. a String which in turn contains a character array etc. . This can be transformed to a FSTStruct backed by a single byte array and an accesswrapper which 'acts' like the objects (with slightly different semantics, see section 'volatile access wrapper').

One can choose to create an access wrapper ('pointer) for each struct allocated, this will save instances in case the flattened Object has at least 2 subobjects (e. one String+its char[]). With this use case one can treat the "complex Object" like any other object, synchronize on it, put it to on-heap collections etc. . The underlying byte array then will be freed when the wrapper is GC'ed.

Another option is to allocate larger chunks of byte[] and store several structs inside this. FSTStructAllocator allows to set the chunksize measured in number of Objects or absolute bytes (may create unused bytes at the end of each chunk).

the byte array will be freed if all access wrapper objects pointing to it are freed.

Note: while the top-level access wrapper object feels like a normal object, references to its substructures are volatile.

   ComplexObject myobject = allocator.newStruct();
   normalHashMap.put( myobject, "this is ok");

   javautilHashMap.put( myobject.getString(), "WRONG !!! volatile reference stored");

   //right:
   StructString tmp = myobject.getString();
   tmp.detach();
   javautilHashMap.put( myobject.getString(), "now that's better");

   //better (create new string):
   javautilHashMap.put( myobject.getString().toString(), "no reference to byte[] stored");

most common case is probably create arrays with only one access object:

Warning regarding persistence, IPC, messaging of FSTStructs

For each struct class an Id is assigned at runtime in the order structs are accessed. If you want to send a Struct to a remote VM or store them to disk, you need to manually define the id mapping, else process A might think {{{1 = MyStruct.class}}} while process 2 thinks {{{1 = MyVeryOtherStruct.class}}}.

You can define the id mapping by adding something like

FSTStructFactory.getInstance().registerClz(MyStruct.class, MyOtherStruct.class, MySubStruct.class,... )

before doing any struct stuff.

ALL structs must be registered for all processes in the exact same order, else you might be hit by access violations which is probably a new experience when programming java ;-) .

"Volatile" accessors

As explained above, a struct consists of a "pointer" to the top level struct objects. Once subelements like arrays or embedded substrucures are accessed, additional access object need to be created for those substructures. As new creation would bog down access performance a lot, FST caches an access wrapper object for each struct class per thread.

   ComplexObject myobject = allocator.newStruct();

   StructString tmp1 = myobject.getString();
   StructString tmp2 = myobject.getOtherString();

   // tmp1 == tmp2 !!!!!!!

in the example above, actually the same object is returned, but the hidden {{{___offset}}} variable points to another position in the underlying byte array. {{{tmp1.detach()}}} actually the pointer instance from the thread local cache, so one can obtain a permanent reference to a struct or part of it. Note each 'detach' call has a cost of a obejct creation.

   ComplexObject myobject = allocator.newStruct();

   StructString tmp1 = myobject.getString();
   tmp1.detach();
   StructString tmp2 = myobject.getOtherString();

   // tmp1 != tmp2

In short: whenever you want to keep a reference on an embedded struct, call detach on it.

Performance

By using C-ish pointer moving, performance is often superior to usual on-heap structures. If one uses the convenience layer, performance of data access degrades with the nesting depth of substructures. Primitive fields on first level e.g. {{{mystruct.getInt()}}} are as fast as normal object field access after the JIT kicked in. {{{mystruct.getSubstructure().getInt()}}} is 2 to 3 times slower.

Benchmark iterating arrays of structs vs. arrays of on heap objects

Details Pending ..

Hacking, Special methods

Depending on naming pattern, instrumentation can provide low level information such as the start of an emebedded int array inside the underlying byte array etc ..

  ..
  int[] array;

  // returns the index of the first integer in the byte array obtained by
  // struct.getBase()
  public int arrayIndex() {
      return -1; // will be generated by instrumentation
  } 

  StructString objectArr[];
  // returns the size of a slot in an embedded Object array
  public int objectArrElementSize() {
      return -1; // will be generated by instrumentation
  } 
  
  // return a volatile pointer pointing to the zero elemenr of an embedded pointer
  // use FSTStruct.cast() to get a typed pointer
  public FSTStruct objectArrPointer() {
      return null; // will be generated by instrumentation
  } 
  
  // same as above, but move existng pointer to begin of array (object reuse)
  public void objectArrPointer(FSTStruct pointer) {
      // will be generated by instrumentation
  } 

  int anInt;
  
  // return the index in the underlying byte array of a struct's field.
  // for arrays and emebedded objects, this points to the header of the array
  // or object.
  // for primitive fields this points directly to the location of the data
  public int anIntStructIndex() {
      return -1; // will be generated by instrumentation
  } 

  // CAS assignment (supported for int and long only)
  public void anIntCAS( int expected, int value ) {
      // will be generated by instrumentation
  }

Code examples

tbd

=Optimization

see also my blog post regarding optimization (missing is unshared mode):
http://java-is-the-new-c.blogspot.de/2013/10/still-using-externalizable-to-get.html

FST defines a set of annotations influencing en/decoding of objects at runtime. Note that default FST (no annotations) is optimized the most. So prepare for a tradeoff speed <=> serialization size. However sometimes it is more important to minimize the size of an object because of bandwith considerations, further processing (remoting in a cluster) etc.

FST has some utility methods which let you measure the effects of your optimization:

FSTConfiguration conf = FSTConfiguration.createDefaultConfiguration();
// .. register some serializers etc.
YourObjectToSerialize ex = new YourObjectToSerialize(); // <= annotate this or not

System.out.println("Size:"+conf.calcObjectSizeBytesNotAUtility(ex));   
System.out.println("Write:"+conf.calcObjectWriteTimeNotAUtility(1000000, ex));
System.out.println("Read:"+conf.calcObjectReadTimeNotAUtility(1000000, ex));

Note that these methods are not intended for usage at runtime ever ! The times are given in microseconds else it would be hard to measure the duration of small objects. In case you test smallish Objects (e.g. 5..10 variables, no collections) consider creating a temporary array of 50 or so instances of your object, else the measuring will be skewed by the time required to initialize the objectstreams itself.

####FSTConfiguration.setShareReferences

for tiny object graphs a big performance gain can be achieved by turning off tracking of identical objects inside the object graph. However this requires all serialized objects to be cycle free (most useful when serializing arguments e.g. marhalling RPC's). Am alternative approach is to define this per-class using the @Flat annotation. See also the related chapter at "Serialization":wiki/Serialization page.

####@Compress

The compress annotation lets FST perform moderate compression on the annotated field. It is currently aknowledged only for Strings and integer arrays.

For Strings an attempt is made to squeeze 2 chars into a byte .. this works good for plain text containing small caps. Don't use it for large caps-only Strings. For normal english strings expect a saving of 10..30%. Note that the String should at least have >12 chars to make this effective. The performance hit for this compression is moderate (far lower than a real compression algorithm). It is guaranteed that the string will not grow in size due to compression attempts.

For integer arrays a detection is done wether one of the following algorithms succeeds in reducing the size:

  • compact int - codes small numbers in 1..3 bytes, but large in 5
  • thin int array - successfull if you have a lot of '0' values in a int array
  • diff int - successful if your integer array contains little volatility (e.g. chart data). Only the first value is written, after that only the difference to the next value is written.
  • offset int - computes min an max of the int array. In case the range is < 65535, the min value and a series of short's is written.

@Conditional

see Serialization page

@OneOf

applicable to String references. Often Strings contain de-facto enumerations, but are not declared so. To prevent the transmission of constant Strings, one can define up to 254 constants at a field reference. If at serialization time the field contains one of the constants, only one byte is written instead of the full string. If the field contains a different string, this one is transmitted. So the list can be incomplete (e.g. just denote frequent values or default values).

...
@OneOf({"EUR", "DOLLAR", "YEN"})
String currency;
...
@OneOf({"BB-", "CC-"})
Object rating;
...

@EqualnessIsBinary

This option may greatly reduce the size of some object graphs. Value type objects such as Person { name, prename, age, id } or ValuePair { int key, String value } frequently denote the same value, but are not represented by the same instance, so default serialization will encode them twice. FST can detect those distinct, but logically identical instances during serialization and manages to write only one instance. At deserialization time, the InputStream is "rewinded" and the single copy is read several times, so at read time you get an exact copy of the object graph you serialized.

Since this consumes some CPU-time, detection is performed only for classes tagged with EqualnessIsBinary. Note that you have to implement equals and hashCode correctly to make this work. Also note if the equals method does not cover all fields of a class, you may get wrong results upon deserializing. last but not least, there must be a significant amount of 'double' objects in your object graph, else you just waste CPU.

Example:

@EqualnessIsBinary
public class ObjectOrientedDataType implements Serializable {

    // encapsulate like a boss
    @Compress
    private String aString="";
    private boolean isCapableOfDoingAnythingMeaningful;
    public ObjectOrientedDataType() {}

    public ObjectOrientedDataType(String valueString) {
        if ( valueString == null ) {
            isCapableOfDoingAnythingMeaningful = false;
            aString = "";
        } else {
            this.aString = valueString;
        }
    }

    public boolean equals( Object o ) {
        if ( o instanceof ObjectOrientedDataType) {
            ObjectOrientedDataType dt = (ObjectOrientedDataType) o;
            return dt.aString.equals(aString) 
                && dt.isCapableOfDoingAnythingMeaningful == isCapableOfDoingAnythingMeaningful;
        }
        return super.equals(o);
    }

    public int hashCode() {
        return aString.hashCode();
    }

    public String toString() {
        return aString;
    }
}

@EqualnessIsIdentity

same as @EqualnessIsBinary, but does not copy the Object at deserialization time, but directly reuses a single instance. This does make sense if the Object is immutable else you might get weird effects ...

FST applies this optimization automatically for Strings. Note that a reference still costs 3 to 6 bytes, so if an object only has one field such as Date, it does not pay off (applies also to @EqualnessIsBinary).

@Flat

This is a pure CPU-saving annotation. If a field referencing some Object is marked as @Flat, FST will not do any effort to reuse this Object. E.g. if a second reference to this Object exists, it will not be restored but contain a copy after deserialization. However if you know in advance there are no identical objects in the object graph, you can save some CPU time. The effects are pretty little, but in case you want to save any nanosecond possible consider this ;). If a class is marked as @Flat, no instance of this class will be checked for equalness or identity. Using this annotation can be important when you serialize small objects as typical in remoting or offheap memory.

@Plain

by default int arrays are saved in a compact form. Actually most integers do not use full 32bit. However worst case (lots of large int numbers), this will increase the size of an int array by 20%. In case you know this in advance you may switch to plain 4-byte per integer encoding using this annotation on an int array. Useful for image or sound data.

@Predict

Can be used at class or field level. At class level, the classes contained in the Predict array, are added to the list of known classes, so no full classnames for those classes are written to the serialization stream. If used at field level, it can save some bytes in case of lose typing. E.g. you have a reference typed 'Entity' and you know it is most likely a 'Employee' or 'Employer', you might add @Predict({Employ.class,Employer.class}) to that field. This way only one byte is used to denote the class of the Object. This also works for fields containing losely typed Collections.

Since FST does a pretty decent job minimizing the size of class names, this optimization makes sense only if you write very short objects, where the transmission of a classname might require more space than the object itself.

Note it is possible to directly register Classes in advance at the FSTConfiguration object. This way the class name is never transmitted. Ofc this must be done at writing and reading side.

@Thin

applicable to int and object arrays, enforces 'Thin' compression as described above in @Compress section.