Speed Up with Fast Java and File Serialization | Javalobby
ince the first version of Java, day-by-day many developers have been trying to achieve at least as good of performance as in C/C++. JVM vendors are doing their best by implementing some new JIT algorithms, but there is still a lot to do, especially in how we use Java.
For example, there is a lot to win in objects<->file serialization - notably in writing/reading objects that can readily fit in the memory. I’ll try to shed some light on that topic.
All the tests were executed on the simple object shown below:
01.
public
class
TestObject
implements
Serializable {
02.
03.
private
long
longVariable;
04.
private
long
[] longArray;
05.
private
String stringObject;
06.
private
String secondStringObject;
//just for testing nulls
07.
08.
/* getters and setters */
09.
}
To be more concise I’ll show only the write methods (though the other way is quite similar, too). Full source code is available on my GitHub (http://github.com/jkubrynski/serialization-tests).
The most standard java serialization (that we all start from) looks like this:
01.
public
void
testWriteBuffered(TestObject test, String fileName)
throws
IOException {
02.
ObjectOutputStream objectOutputStream =
null
;
03.
try
{
04.
FileOutputStream fos =
new
FileOutputStream(fileName);
05.
BufferedOutputStream bos =
new
BufferedOutputStream(fos);
06.
objectOutputStream =
new
ObjectOutputStream(bos);
07.
objectOutputStream.writeObject(test);
08.
}
finally
{
09.
if
(objectOutputStream !=
null
) {
10.
objectOutputStream.close();
11.
}
12.
}
13.
}
The easiest way to speed up the standard serialization is to use the RandomAccessFile object:
01.
public
void
testWriteBuffered(TestObject test, String fileName)
throws
IOException {
02.
ObjectOutputStream objectOutputStream =
null
;
03.
try
{
04.
RandomAccessFile raf =
new
RandomAccessFile(fileName,
"rw"
);
05.
FileOutputStream fos =
new
FileOutputStream(raf.getFD());
06.
objectOutputStream =
new
ObjectOutputStream(fos);
07.
objectOutputStream.writeObject(test);
08.
}
finally
{
09.
if
(objectOutputStream !=
null
) {
10.
objectOutputStream.close();
11.
}
12.
}
The more sophisticated technique is to use the Kryo framework. The difference between the old and the new version is vast. I’ve checked both. Because the performance comparison doesn’t show any spectacular dissimilarities, I’ll focus on the second version as it’s much more user-friendly and even somewhat faster.
01.
private
static
Kryo kryo =
new
Kryo();
// version 2.x
02.
03.
public
void
testWriteBuffered(TestObject test, String fileName)
throws
IOException {
04.
Output output =
null
;
05.
try
{
06.
RandomAccessFile raf =
new
RandomAccessFile(fileName,
"rw"
);
07.
output =
new
Output(
new
FileOutputStream(raf.getFD()), MAX_BUFFER_SIZE);
08.
kryo.writeObject(output, test);
09.
}
finally
{
10.
if
(output !=
null
) {
11.
output.close();
12.
}
13.
}
14.
}
The last option is a solution inspired by Martin Thompson’s article (http://mechanical-sympathy.blogspot.gr/2012/07/native-cc-like-performance-for-java.html). It shows how to play with the memory in the C++ way and in the Java :)
01.
public
void
testWriteBuffered(TestObject test, String fileName)
throws
IOException {
02.
RandomAccessFile raf =
null
;
03.
try
{
04.
MemoryBuffer memoryBuffer =
new
MemoryBuffer(MAX_BUFFER_SIZE);
05.
raf =
new
RandomAccessFile(fileName,
"rw"
);
06.
test.write(memoryBuffer);
07.
raf.write(memoryBuffer.getBuffer());
08.
}
catch
(IOException e) {
09.
if
(raf !=
null
) {
10.
raf.close();
11.
}
12.
}
13.
}
TestObject write method is shown below:
01.
public
void
write(MemoryBuffer unsafeBuffer) {
02.
unsafeBuffer.putLong(longVariable);
03.
unsafeBuffer.putLongArray(longArray);
04.
// we support nulls
05.
boolean
objectExists = stringObject !=
null
;
06.
unsafeBuffer.putBoolean(objectExists);
07.
if
(objectExists) {
08.
unsafeBuffer.putCharArray(stringObject.toCharArray());
09.
}
10.
objectExists = secondStringObject !=
null
;
11.
unsafeBuffer.putBoolean(objectExists);
12.
if
(objectExists) {
13.
unsafeBuffer.putCharArray(secondStringObject.toCharArray());
14.
}
15.
}
Direct memory buffer class (shortened, just to show the idea):
01.
public
class
MemoryBuffer {
02.
// getting Unsafe by reflection
03.
public
static
final
Unsafe unsafe = UnsafeUtil.getUnsafe();
04.
05.
private
final
byte
[] buffer;
06.
07.
private
static
final
long
byteArrayOffset = unsafe.arrayBaseOffset(
byte
[].
class
);
08.
private
static
final
long
longArrayOffset = unsafe.arrayBaseOffset(
long
[].
class
);
09.
/* other offsets */
10.
11.
private
static
final
int
SIZE_OF_LONG =
8
;
12.
/* other sizes */
13.
14.
private
long
pos =
0
;
15.
16.
public
MemoryBuffer(
int
bufferSize) {
17.
this
.buffer =
new
byte
[bufferSize];
18.
}
19.
20.
public
final
byte
[] getBuffer() {
21.
return
buffer;
22.
}
23.
24.
public
final
void
putLong(
long
value) {
25.
unsafe.putLong(buffer, byteArrayOffset + pos, value);
26.
pos += SIZE_OF_LONG;
27.
}
28.
29.
public
final
long
getLong() {
30.
long
result = unsafe.getLong(buffer, byteArrayOffset + pos);
31.
pos += SIZE_OF_LONG;
32.
return
result;
33.
}
34.
35.
public
final
void
putLongArray(
final
long
[] values) {
36.
putInt(values.length);
37.
long
bytesToCopy = values.length <<
3
;
38.
unsafe.copyMemory(values, longArrayOffset, buffer, byteArrayOffset + pos, bytesToCopy);
39.
pos += bytesToCopy;
40.
}
41.
42.
43.
public
final
long
[] getLongArray() {
44.
int
arraySize = getInt();
45.
long
[] values =
new
long
[arraySize];
46.
long
bytesToCopy = values.length <<
3
;
47.
unsafe.copyMemory(buffer, byteArrayOffset + pos, values, longArrayOffset, bytesToCopy);
48.
pos += bytesToCopy;
49.
return
values;
50.
}
51.
52.
/* other methods */
53.
}
Results of many hours of the Caliper’s runs are shown below:
In the end we can draw a few conclusions:
- Unsafe serialization is greater than 23 times faster than standard use of java.io.Serializable
- Use of RandomAccessFile can speed up standard buffered serialization by almost 4 times
- Kryo-dynamic serialization is about 35% slower than the hand-implemented direct buffer.
Finally, as we can see, there is still no golden hammer. For a lot of us, gaining 3000 ns (0.003ms) is not worth writing custom implementations for every object we want to serialize with files. And for standard solutions we’ll mostly choose Kryo. Nevertheless, in low-latency systems, where 100ns seems like an eternity, the choice will be completely different.
'Language > Java' 카테고리의 다른 글
이클립스에서 JAVA API 소스 보기 (0) | 2014.04.04 |
---|---|
Apache Commons Library (0) | 2014.04.04 |
[Java] NIO Channel [펌] (0) | 2014.03.18 |
[Java] 객체 직렬화 Serialization (ObjectInputStream / ObjectOutputStream) (0) | 2014.03.18 |
Log4j 사용법 (0) | 2014.03.15 |