From 1476065b090ec07391f2c1a2560ae700c4ef8fe6 Mon Sep 17 00:00:00 2001 From: Stephen Colebourne Date: Thu, 2 Jan 2025 13:32:02 +0000 Subject: [PATCH] Improve binary serialization performance * Add a dedicated byte array output stream --- .../joda/beans/ser/bin/JodaBeanBinWriter.java | 3 +- .../ser/bin/LinkedByteArrayOutputStream.java | 164 ++++++++++++++++++ .../bin/TestLinkedByteArrayOutputStream.java | 107 ++++++++++++ 3 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/joda/beans/ser/bin/LinkedByteArrayOutputStream.java create mode 100644 src/test/java/org/joda/beans/ser/bin/TestLinkedByteArrayOutputStream.java diff --git a/src/main/java/org/joda/beans/ser/bin/JodaBeanBinWriter.java b/src/main/java/org/joda/beans/ser/bin/JodaBeanBinWriter.java index f88198bd..4a92bb07 100644 --- a/src/main/java/org/joda/beans/ser/bin/JodaBeanBinWriter.java +++ b/src/main/java/org/joda/beans/ser/bin/JodaBeanBinWriter.java @@ -18,7 +18,6 @@ import static org.joda.beans.ser.bin.JodaBeanBinFormat.REFERENCING; import static org.joda.beans.ser.bin.JodaBeanBinFormat.STANDARD; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Objects; @@ -100,7 +99,7 @@ public byte[] write(Bean bean) { * @return the binary data, not null */ public byte[] write(Bean bean, boolean rootType) { - var baos = new ByteArrayOutputStream(1024); + var baos = new LinkedByteArrayOutputStream(); try { write(bean, rootType, baos); } catch (IOException ex) { diff --git a/src/main/java/org/joda/beans/ser/bin/LinkedByteArrayOutputStream.java b/src/main/java/org/joda/beans/ser/bin/LinkedByteArrayOutputStream.java new file mode 100644 index 00000000..66e206e8 --- /dev/null +++ b/src/main/java/org/joda/beans/ser/bin/LinkedByteArrayOutputStream.java @@ -0,0 +1,164 @@ +/* + * 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.bin; + +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. + *

+ * 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. + *

+ * Calling {@link #toByteArray()} returns a single combined byte array. + * Calling {@link #writeTo(OutputStream)} writes the internal arrays without needing to create a combined array. + */ +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. + */ + LinkedByteArrayOutputStream() { + } + + @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++; + } + + @Override + public void write(byte[] bytes, int offset, int length) { + Objects.checkFromIndexSize(offset, length, bytes.length); + var tailRemaining = tail.bytes.length - tail.pos; + if (tailRemaining >= length) { + System.arraycopy(bytes, offset, tail.bytes, tail.pos, length); + tail.pos += length; + } else { + System.arraycopy(bytes, offset, tail.bytes, tail.pos, tailRemaining); + tail.pos += tailRemaining; + var newOffset = offset + tailRemaining; + var newLength = length - tailRemaining; + 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; + } + + @Override + public void write(byte[] b) { + write(b, 0, b.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. + * + * @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(); + } +} diff --git a/src/test/java/org/joda/beans/ser/bin/TestLinkedByteArrayOutputStream.java b/src/test/java/org/joda/beans/ser/bin/TestLinkedByteArrayOutputStream.java new file mode 100644 index 00000000..e5ddba31 --- /dev/null +++ b/src/test/java/org/joda/beans/ser/bin/TestLinkedByteArrayOutputStream.java @@ -0,0 +1,107 @@ +/* + * 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.bin; + +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() { + 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); + } + } + + @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); + } + } + + @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); + } + } + +}