-
-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve binary serialization performance
* Add a dedicated byte array output stream * Performance gain of 20-30% when used * Add Binary and JSON performance tests
- Loading branch information
1 parent
e462e8b
commit 03acad7
Showing
6 changed files
with
489 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
188 changes: 188 additions & 0 deletions
188
src/main/java/org/joda/beans/ser/LinkedByteArrayOutputStream.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
/* | ||
* Copyright 2001-present Stephen Colebourne | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.joda.beans.ser; | ||
|
||
import java.io.IOException; | ||
import java.io.OutputStream; | ||
import java.util.Arrays; | ||
import java.util.HexFormat; | ||
import java.util.Objects; | ||
|
||
/** | ||
* An optimised byte array output stream. | ||
* <p> | ||
* This class holds a number of smaller byte arrays internally. | ||
* Each array is typically 1024 bytes, but if a large byte array is written | ||
* the class will hold it as a single large array. | ||
* <p> | ||
* Calling {@link #toByteArray()} returns a single combined byte array. | ||
* Calling {@link #writeTo(OutputStream)} writes the internal arrays without needing to create a combined array. | ||
* <p> | ||
* This class is not thread-safe. | ||
*/ | ||
public class LinkedByteArrayOutputStream extends OutputStream { | ||
|
||
// segment holding one byte array, the current position in the array, and the next segment when it is full | ||
private static final class ByteSegment { | ||
private final byte[] bytes; | ||
private int pos; | ||
private ByteSegment next; | ||
|
||
private ByteSegment(byte[] bytes) { | ||
this.bytes = bytes; | ||
} | ||
} | ||
|
||
// the head/root segment | ||
private final ByteSegment head = new ByteSegment(new byte[1024]); | ||
// the current tail | ||
private ByteSegment tail = head; | ||
// the total number of bytes written | ||
private int total; | ||
|
||
/** | ||
* Creates an instance. | ||
*/ | ||
public LinkedByteArrayOutputStream() { | ||
} | ||
|
||
//------------------------------------------------------------------------- | ||
/** | ||
* Writes a single byte to the output stream. | ||
* | ||
* @param val the value | ||
*/ | ||
@Override | ||
public void write(int val) { | ||
var tailRemaining = tail.bytes.length - tail.pos; | ||
if (tailRemaining == 0) { | ||
tail.next = new ByteSegment(new byte[1024]); | ||
tail = tail.next; | ||
} | ||
tail.bytes[tail.pos] = (byte) val; | ||
tail.pos++; | ||
total++; | ||
} | ||
|
||
/** | ||
* Writes all or part of a byte array to the output stream. | ||
* | ||
* @param bytes the byte array to write, not null | ||
* @param offset the offset from the start of the array | ||
* @param length the number of bytes to write | ||
* @throws IndexOutOfBoundsException if the offset or length is invalid | ||
*/ | ||
@Override | ||
public void write(byte[] bytes, int offset, int length) { | ||
Objects.checkFromIndexSize(offset, length, bytes.length); | ||
var tailRemaining = tail.bytes.length - tail.pos; | ||
// first part | ||
var firstPartLength = Math.min(tailRemaining, length); | ||
System.arraycopy(bytes, offset, tail.bytes, tail.pos, firstPartLength); | ||
tail.pos += firstPartLength; | ||
// remainder | ||
var newLength = length - firstPartLength; | ||
if (newLength > 0) { | ||
var newOffset = offset + firstPartLength; | ||
if (newLength >= 1024) { | ||
tail.next = new ByteSegment(Arrays.copyOfRange(bytes, newOffset, length)); | ||
} else { | ||
tail.next = new ByteSegment(new byte[1024]); | ||
System.arraycopy(bytes, newOffset, tail.next.bytes, 0, newLength); | ||
} | ||
tail = tail.next; | ||
tail.pos = newLength; | ||
} | ||
total += length; | ||
} | ||
|
||
/** | ||
* Writes a byte array to the output stream. | ||
* | ||
* @param bytes the byte array to write, not null | ||
*/ | ||
@Override | ||
public void write(byte[] bytes) { | ||
write(bytes, 0, bytes.length); | ||
} | ||
|
||
/** | ||
* Writes all the bytes to the specified output stream. | ||
* | ||
* @param out the output stream to write to | ||
* @throws IOException if an IO error occurs | ||
*/ | ||
public void writeTo(OutputStream out) throws IOException { | ||
for (var segment = head; segment != null; segment = segment.next) { | ||
out.write(segment.bytes, 0, segment.pos); | ||
} | ||
} | ||
|
||
/** | ||
* Returns a single byte array containing all the bytes written to the output stream. | ||
* <p> | ||
* The returned array contains a copy of the internal state of this class. | ||
* <p> | ||
* It is not expected that callers will call this method multiple times, although it is safe to do so. | ||
* | ||
* @return the combined byte array | ||
*/ | ||
public byte[] toByteArray() { | ||
var result = new byte[total]; | ||
var pos = 0; | ||
for (var segment = head; segment != null; segment = segment.next) { | ||
System.arraycopy(segment.bytes, 0, result, pos, segment.pos); | ||
pos += segment.pos; | ||
} | ||
return result; | ||
} | ||
|
||
/** | ||
* Gets the current number of bytes written. | ||
* | ||
* @return the number of bytes written | ||
*/ | ||
public int size() { | ||
return total; | ||
} | ||
|
||
/** | ||
* A no-op, as this class does not need flushing. | ||
*/ | ||
@Override | ||
public void flush() { | ||
} | ||
|
||
/** | ||
* A no-op, as this class does not need closing. | ||
*/ | ||
@Override | ||
public void close() { | ||
} | ||
|
||
/** | ||
* Returns a hex-formatted string of the bytes that have been written. | ||
*/ | ||
@Override | ||
public String toString() { | ||
var hex = HexFormat.of(); | ||
var buf = new StringBuilder(total * 2); | ||
for (var segment = head; segment != null; segment = segment.next) { | ||
hex.formatHex(buf, segment.bytes, 0, segment.pos); | ||
} | ||
return buf.toString(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
src/test/java/org/joda/beans/ser/TestLinkedByteArrayOutputStream.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
/* | ||
* Copyright 2001-present Stephen Colebourne | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.joda.beans.ser; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
import java.util.Arrays; | ||
|
||
import org.junit.jupiter.api.Test; | ||
|
||
/** | ||
* Test {@link LinkedByteArrayOutputStream}. | ||
*/ | ||
class TestLinkedByteArrayOutputStream { | ||
|
||
@Test | ||
void test_empty() { | ||
try (var test = new LinkedByteArrayOutputStream()) { | ||
assertThat(test).hasToString(""); | ||
assertThat(test.toByteArray()).isEqualTo(new byte[0]); | ||
assertThat(test.size()).isEqualTo(0); | ||
} | ||
} | ||
|
||
@Test | ||
void test_writeByte() { | ||
try (var test = new LinkedByteArrayOutputStream()) { | ||
test.write(33); | ||
assertThat(test).hasToString("21"); | ||
assertThat(test.toByteArray()).isEqualTo(new byte[] {33}); | ||
assertThat(test.size()).isEqualTo(1); | ||
} | ||
} | ||
|
||
@Test | ||
void test_writeByte_growCapacity() { | ||
try (var test = new LinkedByteArrayOutputStream()) { | ||
test.write(new byte[1024]); | ||
test.write(33); | ||
assertThat(test.toString()).hasSize(2050).endsWith("0021"); | ||
assertThat(test.toByteArray()).hasSize(1025).endsWith(new byte[] {33}); | ||
} | ||
} | ||
|
||
@Test | ||
void test_writeByteArray_empty() { | ||
try (var test = new LinkedByteArrayOutputStream()) { | ||
test.write(new byte[0]); | ||
assertThat(test).hasToString(""); | ||
assertThat(test.toByteArray()).isEqualTo(new byte[0]); | ||
assertThat(test.size()).isEqualTo(0); | ||
} | ||
} | ||
|
||
@Test | ||
void test_writeByteArray_normal() { | ||
try (var test = new LinkedByteArrayOutputStream()) { | ||
var bytes = new byte[] {33, 34, 35, 36, 37}; | ||
test.write(bytes, 1, 3); | ||
assertThat(test).hasToString("222324"); | ||
assertThat(test.toByteArray()).isEqualTo(new byte[] {34, 35, 36}); | ||
assertThat(test.size()).isEqualTo(3); | ||
} | ||
} | ||
|
||
@Test | ||
void test_writeByteArray_growCapacityExact() { | ||
try (var test = new LinkedByteArrayOutputStream()) { | ||
var bytes = new byte[] {33, 34, 35, 36, 37}; | ||
test.write(new byte[1024]); | ||
test.write(bytes, 1, 4); | ||
assertThat(test.toString()).hasSize(2056).endsWith("22232425"); | ||
assertThat(test.toByteArray()).hasSize(1028).endsWith(new byte[] {34, 35, 36, 37}); | ||
assertThat(test.size()).isEqualTo(1028); | ||
assertThat(test.toByteArray()).isEqualTo(test.toByteArray()); | ||
} | ||
} | ||
|
||
@Test | ||
void test_writeByteArray_growCapacitySplit() { | ||
try (var test = new LinkedByteArrayOutputStream()) { | ||
var bytes = new byte[] {33, 34, 35, 36, 37}; | ||
test.write(new byte[1022]); | ||
test.write(bytes, 0, 3); | ||
assertThat(test.toString()).hasSize(2050).endsWith("212223"); | ||
assertThat(test.toByteArray()).hasSize(1025).endsWith(new byte[] {33, 34, 35}); | ||
assertThat(test.size()).isEqualTo(1025); | ||
assertThat(test.toByteArray()).isEqualTo(test.toByteArray()); | ||
} | ||
} | ||
|
||
@Test | ||
void test_writeByteArray_large() { | ||
try (var test = new LinkedByteArrayOutputStream()) { | ||
var bytes = new byte[2048]; | ||
Arrays.fill(bytes, (byte) 33); | ||
test.write(new byte[1022]); | ||
test.write(bytes); | ||
test.write(34); | ||
assertThat(test.toString()).hasSize((1022 + 2048 + 1) * 2).endsWith("212122"); | ||
assertThat(test.toByteArray()).hasSize(1022 + 2048 + 1).endsWith(new byte[] {33, 33, 34}); | ||
assertThat(test.size()).isEqualTo(1022 + 2048 + 1); | ||
assertThat(test.toByteArray()).isEqualTo(test.toByteArray()); | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.