Into the mist of serialization myths

Performance myth: serialVersionUID improves Java serialization performance

June 27, 2003

Q: Does setting the serialVersionUID class field improve Java serialization performance?

A: I have heard this performance "tip" from several colleagues and keep running across it in various Java forums. In this Java Q&A installment, we discover that it is mostly an urban legend.

The question refers to the following problem: when deserializing an object of class X, Java must establish that the incoming data is sufficiently compatible with the local class X definition. This is accomplished by comparing the so-called stream-unique identifiers (SUIDs) of the incoming and local class definitions. If the two SUIDs do not match, deserialization fails. If you don't do anything, the SUID is computed as a hash of various class elements: class name, implemented interfaces, declared nonstatic, nontransient fields, declared nonprivate methods, and so on. But it is also possible to take control of this value by declaring the following class field:

    private static final long serialVersionUID = ;

The actual value assigned to serialVersionUID does not matter as long as you remember to change it with every serialization-incompatible change to the class.

Besides giving you explicit control over class versioning, placing an explicit SUID value in the serialVersionUID field supposedly saves many CPU cycles during serialization. That's the folklore, in any case. Let's see if it's really true.

A few published performance analyses discovered "surprising" performance savings related to serialVersionUID optimization. Some of them suffer from several shortcomings:

  • They use coarse-grained timers like System.currentTimeMillis(), necessitating long repeat loops in code and raking up tons of temporary objects on the heap, possibly causing garbage collection to bias all results

  • They do not discard timings obtained from initial runs of their test code (see "Watch Your HotSpot Compiler Go" for why this is important)

To get better results, I put together the following simple test class:

public class SerialVersionUIDTest
{
    public static void main (final String [] args)
        throws Exception
    {
        final DecimalFormat format = new DecimalFormat ("#.000");
        
        // Create a couple of timers to keep track of object
        // serialization/deserialization:
        s_wtimer = TimerFactory.newTimer ();
        s_rtimer = TimerFactory.newTimer ();
          
        // This is my test object:      
        final TestClass testobj = new TestClass ();
        
        // ITimer/perftest() warmup:
        for (int i = 0; i < 3000; ++ i)
        {
            perftest (testobj);
            
            s_wtimer.getDuration ();
            s_wtimer.reset ();
            s_rtimer.reset ();
        }
        
        final int repeats = 500;
        final double [] durations = new double [repeats];
        final Object [] holder = new Object [repeats];
        
        for (int r = 0; r < repeats; ++ r)
        {
            Object clone = perftest (testobj);
                        
            holder [r] = clone; // Retain this reference to minimize GC activity
            durations [r] = s_wtimer.getDuration () + s_rtimer.getDuration ();
            
            s_wtimer.reset ();
            s_rtimer.reset ();
        }
        
        // Compute the average:
        double avg = 0.0;
        for (int i = 0; i < repeats; ++ i) avg += durations [i];
        avg /= repeats;
        
        // Sort to get percentile metrics:
        Arrays.sort (durations);
        
        System.out.println ("[min/avg/95th percentile, ms] min: "
            + format.format (durations [0])
            + ", avg: " + format.format (avg)
            + ", 95%: " + format.format (durations [(95 * repeats)/100]));
    }
    
    /*
     * This is the actual test method. It clones 'obj' by in-memory serialization.
     * Only the costs of writeObject()/readObject() are tallied.
     */
    private static Object perftest (final Object obj)
        throws IOException, ClassNotFoundException
    {
        s_out.reset ();
        ObjectOutputStream oout = new ObjectOutputStream (s_out);
        
        s_wtimer.start ();
        oout.writeObject (obj);
        s_wtimer.stop ();
        
        ObjectInputStream in = new ObjectInputStream (new ByteArrayInputStream (s_out.toByteArray ()));
        
        s_rtimer.start ();
        final Object result = in.readObject ();
        s_rtimer.stop ();
        
        return result;
    }
    
    private static ITimer s_wtimer, s_rtimer;
    private static final ByteArrayOutputStream s_out = new ByteArrayOutputStream (32 * 1024);
    
} // End of class

Regular Java Q&A column readers should see familiar patterns above. I used the high-resolution timer developed in "My Kingdom for a Good Timer!." Even though the guts of that library are Java Native Interface (JNI) code, I nevertheless warm up the accompanying ITimer byte code via the usual loop whose repeat count is higher than the client HotSpot's native compilation threshold (again, see "Watch Your HotSpot Compiler Go"). My test method is perftest(), which is warmed up within the same loop: I am interested in my runtime's steady state, not what happens on the first invocation. The SerialVersionUIDTest version you can download also cleans up the JVM heap before commencing the timing loop.

The perftest() method serializes and deserializes an instance of TestClass:

public final class TestClass extends TestBaseClass
                             implements Serializable
{
    public TestClass ()
    {
        m_int = 4;
        
        m_object1 = new TestBaseClass ();
        m_object2 = m_object1;
    
        m_objects = new Object [m_int];
        for (int i = 0; i < m_objects.length; ++ i)
            m_objects [i] = new TestBaseClass ();
    }
    
    public int getInt ()
    {
        return m_int;
    }
    
    public Object getObject1 ()
    {
        return m_object1;
    }
    
    ... other accessor methods...
        
    // Uncomment this here and in TestBaseClass to re-run
    // SerialVersionUIDTest with the "optimization" enabled:
      
    //private static final long serialVersionUID = 1;
    private final int m_int;        
    private final Object m_object1, m_object2;
    private final Object [] m_objects;
} // End of class
public class TestBaseClass
             implements Serializable
{
    public TestBaseClass ()
    {
        m_byte = (byte) 1;
        m_short = (short) 2;
        m_long = 3L;
        m_float = 4.0F;
        m_double = 5.0;
        m_char = '6';
        m_boolean = true;
        m_int = 8;
        m_string = "some string";
        
        m_ints = new int [m_int];
        for (int i = 0; i < m_ints.length; ++ i) m_ints [i] = m_int;
        
        m_strings = new String [m_int];
        for (int i = 0; i < m_strings.length; ++ i)
            m_strings [i] = "string value #" + i;
    }
    public byte getByte ()
    {
        return m_byte;
    }
    
    public short getShort ()
    {
        return m_short;
    }
    
    ... other accessor methods ...
    // Uncomment this here and in TestClass to re-run
    // SerialVersionUIDTest with the "optimization" enabled:
    
    //private static final long serialVersionUID = 1;
    private final byte m_byte;
    private final short m_short;
    private final long m_long;
    private final float m_float;
    private final double m_double;
    private final char m_char;
    private final boolean m_boolean;
    private final int m_int;
    private final int [] m_ints;
    private final String m_string;
    private final String [] m_strings;
    
} // End of class 

TestClass and its superclass, TestBaseClass, are distant relatives of their namesakes used for cloning tests in "Attack of the Clones." Together they create a reasonably representative Java object. This object has fields of different types and various accessor methods, and it involves inheritance and aggregation: in short, it gives something for the SUID computation to work on. The TestClass instance's serialized size is about 1,800 bytes.

SerialVersionUIDTest records the costs of readObject() and writeObject() method calls and generates some simple statistics: minimum, average, and 95th percentile of the combined cost of serializing and deserializing a TestClass instance. (I use a percentile instead of the maximum to filter outlying data points due to HotSpot/GC (garbage collection) activity.)

Some results, anticlimactic as they may be

Let's try this in Sun Microsystems' Java Runtime Environment (JRE) 1.4.1 (on a Windows NT/dual 550-MHz CPU machine; all timings are in milliseconds):

>java -Xms100m -Xmx100m -cp hrtlib.jar;... SerialVersionUIDTest
[min/avg/95th percentile, ms] min: 1.692, avg: 1.809, 95%: 1.897

Now, let's add private static final long serialVersionUID = 1; to both TestClass and TestBaseClass and try again:

>java -Xms100m -Xmx100m -cp hrtlib.jar;... SerialVersionUIDTest
[min/avg/95th percentile, ms] min: 1.703, avg: 1.817, 95%: 1.855

You can see why it is hard to get excited about these new numbers. The cost differences are not even above the random data noise. If anything, the new numbers appear to speak against the optimization.

Maybe only older JVMs benefit from the serialVersionUID idea? For completeness, here are the results for three Sun JVM versions running the same test code on the same machine:

Serialization cost in milliseconds

JVM version Minimum Average 95th percentile
JVM 1.4 without serialVersionUID 1.692 1.809 1.897
JVM 1.4 with serialVersionUID 1.703 1.817 1.855
JVM 1.3 without serialVersionUID 1.623 1.660 1.773
JVM 1.3 with serialVersionUID 1.628 1.657 1.769
JVM 1.2 without serialVersionUID 2.055 2.122 2.220
JVM 1.2 with serialVersionUID 2.083 2.142 2.231

As you can see, the conclusions remain basically the same across all Sun JVM implementations.

Explanations

The reason for the above behavior is quite straightforward. It would be very naive for a JVM to recompute SUIDs for the same classes over and over again every time you serialize an object. If you examine the JDK sources, you will discover that SUID values are computed once and subsequently kept in a soft cache (the cache is soft so as not to impede class unloading). In Java 2 Platform, Standard Development Kit (J2SDK) 1.2 and 1.3, this cache is the field descriptorFor of java.io.ObjectStreamClass (this class acts as a Class serialization descriptor and holds the Class's SUID value):

    /*
     * Cache of Class -> ClassDescriptor Mappings.
     */
    static private ObjectStreamClassEntry[] descriptorFor = new ObjectStreamClassEntry[61];

And in J2SDK 1.4, the equivalent cache is maintained by an instance of sun.misc.SoftCache:

    /** cache mapping local classes -> descriptors */
    private static final SoftCache localDescs = new SoftCache(10);

Things are now much clearer: the (supposedly) expensive SUID computation was done only once for each of my two test classes. This happened inside my warm-up loop, long before the timing measurements started. The steady state of my runtime does not incur the cost of SUID calculation at all.

Declaring an explicit serialVersionUID field in your classes therefore saves you some CPU time only the very first time the JVM process serializes a given Class. Optimizing this (rare) case is precisely the opposite of what smart optimization is all about. This also means that the steady state cost of serialization is completely dominated by the cost of reading and writing actual class data (so other techniques, such as reducing the amount of class metadata sent by implementing Externalizable and optimizing the wire data layout, still apply).

The cache map is soft, and no documentation details explain when its entries might clear, but this should rarely happen: perhaps for very stale class descriptors or when the JVM is low on memory. In both cases, the extra hit of recomputing the SUID will rarely be your primary performance concern.

Out of curiosity, I instrumented the java.io.ObjectStreamClass just to see how much time that initial SUID computation takes: this cost turned out to range from 5 to 50 milliseconds in my experiments. This is not a trivial amount to try to save in general, but again, if this is your application's bottleneck, then something is very wrong with your overall design.

The old adage is still true

As it has been said time and time again, you should always profile your application first and optimize the actual hot spots found. Explicitly versioning a class via serialVersionUID requires more code maintenance and is more error prone. There are many valid reasons for using serialVersionUID (protecting against compiler differences, establishing backward serialization compatibility, etc.), but performance is not one of them.

Vladimir Roubtsov has programmed in a variety of languages for more than 13 years, including Java since 1995. Currently, he develops enterprise software as a senior engineer for Trilogy in Austin, Texas.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more