serialVersionUID improves Java serialization performance

来源:互联网 发布:网络歌曲2016伤感情歌 编辑:程序博客网 时间:2024/05/23 13:23

QDoes setting the serialVersionUID class field improve Java serialization performance?

AI 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 = <some value>;


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.

Author Bio

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.


Resources
  • Download the complete library that accompanies this article
    http://www.javaworld.com/javaworld/javaqa/2003-06/mythser/02-qa-0627-mythser.zip
  • "Watch Your HotSpot Compiler Go," Vladimir Roubtsov (JavaWorld, April 2003)
    http://www.javaworld.com/javaworld/javaqa/2003-04/01-qa-0411-hotspot.html
  • "My Kingdom for a Good Timer!" Vladimir Roubtsov (JavaWorld, January 2003)
    http://www.javaworld.com/javaworld/javaqa/2003-01/01-qa-0110-timing.html
  • "Attack of the Clones," Vladimir Roubtsov (JavaWorld, January 2003)
    http://www.javaworld.com/javaworld/javaqa/2003-01/02-qa-0124-clone.html
  • The Java Object Serialization Specification contains more details on SUIDs and the algorithms behind their computation
    http://java.sun.com/j2se/1.4.1/docs/guide/serialization/spec/class.doc6.html#4100
  • Want more? See the Java Q&A index page for the full Q&A catalog
    http://www.javaworld.com/columns/jw-qna-index.shtml
  • For more than 100 insightful Java tips, visit JavaWorld's Java Tips index page
    http://www.javaworld.com/columns/jw-tips-index.shtml
  • Visit the Core Java section of JavaWorld's Topical Index
    http://www.javaworld.com/channel_content/jw-core-index.shtml
  • Browse the Java Virtual Machine section of JavaWorld's Topical Index
    http://www.javaworld.com/channel_content/jw-jvm-index.shtml
  • Browse the Performance Tuning section of JavaWorld's Topical Index
    http://www.javaworld.com/channel_content/jw-performance-index.shtml
  • Sign up for JavaWorld's free weekly email newsletters
    http://www.javaworld.com/subscribe
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 联想V形底座不好用怎么办 华为手环不计步怎么办 小米手环不计步怎么办 小米手环骑自行车不计步数怎么办 放逐之城着火了怎么办 城市天际线地价过低怎么办 放逐之城铁采完怎么办 车钥匙反锁车内怎么办 眼镜用热水洗了怎么办 眼镜放平后眼镜腿不平怎么办 瞄准镜十字歪了怎么办 瞄准镜调到底了怎么办 墨镜镜片刮花了怎么办 usb小风扇不转怎么办 金属眼镜压歪了怎么办 眼镜被电焊打了怎么办 电焊闪的眼睛疼怎么办 烧了电焊眼睛疼怎么办 用了电焊眼睛痛怎么办 烧电焊脸上红痛怎么办 眼睛让电焊晃了怎么办 眼被电焊打了怎么办 眼镜弹簧腿坏了怎么办 眼镜框铰链坏了怎么办 金属眼镜框歪了怎么办 眼镜框螺丝断了怎么办 眼镜被压变形了怎么办 金属眼镜腿断了怎么办 眼镜弹簧腿断了怎么办 眼镜腿螺丝太紧怎么办 眼镜金属柄断了怎么办 金属眼镜腿折了怎么办 眼镜腿中间断了怎么办 塑料眼镜腿断了怎么办 眼镜上的螺丝拧不紧怎么办 眼镜的把坏了怎么办 把眼镜坐坏了怎么办 梦见眼镜腿掉了怎么办 眼镜的腿掉了怎么办 眼镜腿的螺丝掉了怎么办 爱大爱眼镜掉腿了怎么办