Skip to content

Commit 144e287

Browse files
authored
Optimize BSON decoding (#1667)
- Removed redundant String copying, replacing with view-based access. - Added scratch buffer read optimization. JAVA-5842
1 parent 52910f4 commit 144e287

File tree

9 files changed

+778
-28
lines changed

9 files changed

+778
-28
lines changed

bson/src/main/org/bson/ByteBuf.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ public interface ByteBuf {
192192
* @return {@code true} if, and only if, this buffer is backed by an array and is not read-only
193193
* @since 5.5
194194
*/
195-
boolean hasArray();
195+
boolean isBackedByArray();
196196

197197
/**
198198
* Returns the offset of the first byte within the backing byte array of

bson/src/main/org/bson/ByteBufNIO.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public byte[] array() {
133133
}
134134

135135
@Override
136-
public boolean hasArray() {
136+
public boolean isBackedByArray() {
137137
return buf.hasArray();
138138
}
139139

bson/src/main/org/bson/io/ByteBufferBsonInput.java

+46-17
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@
3333
public class ByteBufferBsonInput implements BsonInput {
3434

3535
private static final String[] ONE_BYTE_ASCII_STRINGS = new String[Byte.MAX_VALUE + 1];
36+
/* A dynamically sized scratch buffer, that is reused across BSON String reads:
37+
* 1. Reduces garbage collection by avoiding new byte array creation.
38+
* 2. Improves cache utilization through temporal locality.
39+
* 3. Avoids JVM allocation and zeroing cost for new memory allocations.
40+
*/
41+
private byte[] scratchBuffer;
42+
3643

3744
static {
3845
for (int b = 0; b < ONE_BYTE_ASCII_STRINGS.length; b++) {
@@ -127,15 +134,12 @@ public String readString() {
127134

128135
@Override
129136
public String readCString() {
130-
int mark = buffer.position();
131-
skipCString();
132-
int size = buffer.position() - mark;
133-
buffer.position(mark);
137+
int size = computeCStringLength(buffer.position());
134138
return readString(size);
135139
}
136140

137-
private String readString(final int size) {
138-
if (size == 2) {
141+
private String readString(final int bsonStringSize) {
142+
if (bsonStringSize == 2) {
139143
byte asciiByte = buffer.get(); // if only one byte in the string, it must be ascii.
140144
byte nullByte = buffer.get(); // read null terminator
141145
if (nullByte != 0) {
@@ -146,26 +150,51 @@ private String readString(final int size) {
146150
}
147151
return ONE_BYTE_ASCII_STRINGS[asciiByte]; // this will throw if asciiByte is negative
148152
} else {
149-
byte[] bytes = new byte[size - 1];
150-
buffer.get(bytes);
151-
byte nullByte = buffer.get();
152-
if (nullByte != 0) {
153-
throw new BsonSerializationException("Found a BSON string that is not null-terminated");
153+
if (buffer.isBackedByArray()) {
154+
int position = buffer.position();
155+
int arrayOffset = buffer.arrayOffset();
156+
int newPosition = position + bsonStringSize;
157+
buffer.position(newPosition);
158+
159+
byte[] array = buffer.array();
160+
if (array[arrayOffset + newPosition - 1] != 0) {
161+
throw new BsonSerializationException("Found a BSON string that is not null-terminated");
162+
}
163+
return new String(array, arrayOffset + position, bsonStringSize - 1, StandardCharsets.UTF_8);
164+
} else if (scratchBuffer == null || bsonStringSize > scratchBuffer.length) {
165+
int scratchBufferSize = bsonStringSize + (bsonStringSize >>> 1); //1.5 times the size
166+
scratchBuffer = new byte[scratchBufferSize];
167+
}
168+
169+
buffer.get(scratchBuffer, 0, bsonStringSize);
170+
if (scratchBuffer[bsonStringSize - 1] != 0) {
171+
throw new BsonSerializationException("BSON string not null-terminated");
154172
}
155-
return new String(bytes, StandardCharsets.UTF_8);
173+
return new String(scratchBuffer, 0, bsonStringSize - 1, StandardCharsets.UTF_8);
156174
}
157175
}
158176

159177
@Override
160178
public void skipCString() {
161179
ensureOpen();
162-
boolean checkNext = true;
163-
while (checkNext) {
164-
if (!buffer.hasRemaining()) {
165-
throw new BsonSerializationException("Found a BSON string that is not null-terminated");
180+
int pos = buffer.position();
181+
int length = computeCStringLength(pos);
182+
buffer.position(pos + length);
183+
}
184+
185+
private int computeCStringLength(final int prevPos) {
186+
ensureOpen();
187+
int pos = buffer.position();
188+
int limit = buffer.limit();
189+
190+
while (pos < limit) {
191+
if (buffer.get(pos++) == 0) {
192+
return (pos - prevPos);
166193
}
167-
checkNext = buffer.get() != 0;
168194
}
195+
196+
buffer.position(pos);
197+
throw new BsonSerializationException("Found a BSON string that is not null-terminated");
169198
}
170199

171200
@Override

driver-core/src/main/com/mongodb/internal/connection/ByteBufferBsonOutput.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ protected int writeCharacters(final String str, final boolean checkNullTerminati
399399
int curBufferLimit = curBuffer.limit();
400400
int remaining = curBufferLimit - curBufferPos;
401401

402-
if (curBuffer.hasArray()) {
402+
if (curBuffer.isBackedByArray()) {
403403
byte[] dst = curBuffer.array();
404404
int arrayOffset = curBuffer.arrayOffset();
405405
if (remaining >= str.length() + 1) {

driver-core/src/main/com/mongodb/internal/connection/CompositeByteBuf.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ public byte[] array() {
214214
}
215215

216216
@Override
217-
public boolean hasArray() {
217+
public boolean isBackedByArray() {
218218
return false;
219219
}
220220

driver-core/src/main/com/mongodb/internal/connection/ResponseBuffers.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,15 @@ <T extends BsonDocument> T getResponseDocument(final int messageId, final Decode
5353
}
5454

5555
/**
56-
* Returns a read-only buffer containing the response body. Care should be taken to not use the returned buffer after this instance has
56+
* Returns a buffer containing the response body. Care should be taken to not use the returned buffer after this instance has
5757
* been closed.
5858
*
59-
* @return a read-only buffer containing the response body
59+
* NOTE: do not modify this buffer, it is being made writable for performance reasons to avoid redundant copying.
60+
*
61+
* @return a buffer containing the response body
6062
*/
6163
public ByteBuf getBodyByteBuffer() {
62-
return bodyByteBuffer.asReadOnly();
64+
return bodyByteBuffer;
6365
}
6466

6567
public void reset() {

driver-core/src/main/com/mongodb/internal/connection/netty/NettyByteBuf.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public byte[] array() {
125125
}
126126

127127
@Override
128-
public boolean hasArray() {
128+
public boolean isBackedByArray() {
129129
return proxied.hasArray();
130130
}
131131

0 commit comments

Comments
 (0)