postgresql-jdbc.git / summary / log / commit / refs

commit    b97031ecf29ce4e80aadcf80fe3e262aa4422423
Author:   Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
Date:     Sun Dec 03 10:38:24 2023 +0000

    perf: reduce array allocation in ByteStreamWriter.writeTo(ByteStreamTarget)
    
    Previously, the implementation always created a Channel with Channels.newChannel(outputStream),
    however, OpenJDK (e.g. 22) still allocates arrays.
    
    The improvement is to access the arrays in case ByteBuffer allows so.
    DirectByteBuffer and read-only ByteBuffers forbid array access.


.../util/ByteBufferByteStreamWriter.java | 12 +++-- .../util/ByteBuffersByteStreamWriter.java | 28 ++++++++++-- .../postgresql/test/util/ByteStreamWriterTest.java | 51 +++++++++++++++++++++- 3 files changed, 82 insertions(+), 9 deletions(-) diff --git a/pgjdbc/src/main/java/org/postgresql/util/ByteBufferByteStreamWriter.java b/pgjdbc/src/main/java/org/postgresql/util/ByteBufferByteStreamWriter.java index be25be54..f0a8aa03 100644 --- a/pgjdbc/src/main/java/org/postgresql/util/ByteBufferByteStreamWriter.java +++ b/pgjdbc/src/main/java/org/postgresql/util/ByteBufferByteStreamWriter.java @@ -36,13 +36,17 @@ public class ByteBufferByteStreamWriter implements ByteStreamWriter { @Override public void writeTo(ByteStreamTarget target) throws IOException { + if (buf.hasArray()) { + // Avoid copying the array if possible + target.getOutputStream() + .write(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining()); + return; + } + // this _does_ involve some copying to a temporary buffer, but that's unavoidable // as OutputStream itself only accepts single bytes or heap allocated byte arrays - WritableByteChannel c = Channels.newChannel(target.getOutputStream()); - try { + try (WritableByteChannel c = Channels.newChannel(target.getOutputStream())) { c.write(buf); - } finally { - c.close(); } } } diff --git a/pgjdbc/src/main/java/org/postgresql/util/ByteBuffersByteStreamWriter.java b/pgjdbc/src/main/java/org/postgresql/util/ByteBuffersByteStreamWriter.java index ea8feb53..9a954307 100644 --- a/pgjdbc/src/main/java/org/postgresql/util/ByteBuffersByteStreamWriter.java +++ b/pgjdbc/src/main/java/org/postgresql/util/ByteBuffersByteStreamWriter.java @@ -6,6 +6,7 @@ package org.postgresql.util; import java.io.IOException; +import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.WritableByteChannel; @@ -40,11 +41,30 @@ class ByteBuffersByteStreamWriter implements ByteStreamWriter { @Override public void writeTo(ByteStreamTarget target) throws IOException { - // this _does_ involve some copying to a temporary buffer, but that's unavoidable - // as OutputStream itself only accepts single bytes or heap allocated byte arrays - try (WritableByteChannel c = Channels.newChannel(target.getOutputStream());) { + boolean allArraysAreAccessible = true; + for (ByteBuffer buffer : buffers) { + if (!buffer.hasArray()) { + allArraysAreAccessible = false; + break; + } + } + + OutputStream os = target.getOutputStream(); + if (allArraysAreAccessible) { + for (ByteBuffer buffer : buffers) { + os.write(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); + } + return; + } + // Channels.newChannel does not buffer writes, so we can mix writes to the channel with writes + // to the OutputStream + try (WritableByteChannel c = Channels.newChannel(os);) { for (ByteBuffer buffer : buffers) { - c.write(buffer); + if (buffer.hasArray()) { + os.write(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); + } else { + c.write(buffer); + } } } } diff --git a/pgjdbc/src/test/java/org/postgresql/test/util/ByteStreamWriterTest.java b/pgjdbc/src/test/java/org/postgresql/test/util/ByteStreamWriterTest.java index 6f3d771e..34069e31 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/util/ByteStreamWriterTest.java +++ b/pgjdbc/src/test/java/org/postgresql/test/util/ByteStreamWriterTest.java @@ -36,9 +36,12 @@ public class ByteStreamWriterTest extends BaseTest4 { private ByteBuffer testData(int size) { ByteBuffer data = ByteBuffer.allocate(size); Random random = new Random(31459); - while (data.remaining() > 0) { + while (data.remaining() > 8) { data.putLong(random.nextLong()); } + while (data.remaining() > 0) { + data.put((byte) (random.nextInt() % 256)); + } data.rewind(); return data; } @@ -95,6 +98,52 @@ public class ByteStreamWriterTest extends BaseTest4 { validateContent(testData); } + @Test + public void testLength37b() throws Exception { + ByteBuffer testData = testData(37); + insertStream(testData); + validateContent(testData); + } + + @Test + public void testLength2KbReadOnly() throws Exception { + ByteBuffer testData = testData(2 * 1024); + // Read-only buffer does not provide access to the array, so we test it separately + insertStream(testData.asReadOnlyBuffer()); + validateContent(testData); + } + + @Test + public void testTwoBuffers() throws Exception { + ByteBuffer testData = testData(20); + ByteBuffer part1 = testData.duplicate(); + part1.position(0); + part1.limit(9); + ByteBuffer part2 = testData.duplicate(); + part2.position(part1.limit()); + part2.limit(testData.limit()); + // Read-only buffer does not provide access to the array, so we test it separately + insertStream(ByteStreamWriter.of(part1, part2)); + validateContent(testData); + } + + @Test + public void testThreeBuffersWithReadonly() throws Exception { + ByteBuffer testData = testData(20); + ByteBuffer part1 = testData.duplicate(); + part1.position(0); + part1.limit(9); + ByteBuffer part2 = testData.duplicate(); + part2.position(part1.limit()); + part2.limit(15); + ByteBuffer part3 = testData.duplicate(); + part3.position(part2.limit()); + part3.limit(testData.limit()); + // Read-only buffer does not provide access to the array, so we test it separately + insertStream(ByteStreamWriter.of(part1, part2.asReadOnlyBuffer(), part3)); + validateContent(testData); + } + @Test public void testLength10Kb() throws Exception { ByteBuffer testData = testData(10 * 1024); [parent: b419750929ae]