Skip to content

Commit

Permalink
Improve binary serialization performance
Browse files Browse the repository at this point in the history
* Add a dedicated byte array output stream
  • Loading branch information
jodastephen committed Jan 2, 2025
1 parent e462e8b commit 1476065
Show file tree
Hide file tree
Showing 3 changed files with 272 additions and 2 deletions.
3 changes: 1 addition & 2 deletions src/main/java/org/joda/beans/ser/bin/JodaBeanBinWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
164 changes: 164 additions & 0 deletions src/main/java/org/joda/beans/ser/bin/LinkedByteArrayOutputStream.java
Original file line number Diff line number Diff line change
@@ -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.
* <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.
*/
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();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}

}

0 comments on commit 1476065

Please sign in to comment.