Skip to content

Commit 6fa95c2

Browse files
authored
Eliminate temporary buffer allocations during bson serialization (#1628)
JAVA-5786 --------- Co-authored-by: Viacheslav Babanin <[email protected]>
1 parent 23fe359 commit 6fa95c2

File tree

3 files changed

+198
-37
lines changed

3 files changed

+198
-37
lines changed

bson/src/main/org/bson/BsonBinaryWriter.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ public void doWriteNull() {
289289
public void doWriteObjectId(final ObjectId value) {
290290
bsonOutput.writeByte(BsonType.OBJECT_ID.getValue());
291291
writeCurrentName();
292-
bsonOutput.writeBytes(value.toByteArray());
292+
bsonOutput.writeObjectId(value);
293293
}
294294

295295
@Override

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

+79-29
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@
1818

1919
import org.bson.ByteBuf;
2020
import org.bson.ByteBufNIO;
21+
import org.bson.types.ObjectId;
2122

2223
import java.io.IOException;
2324
import java.io.OutputStream;
25+
import java.nio.Buffer;
2426
import java.nio.ByteBuffer;
2527
import java.util.Arrays;
28+
import java.util.Collections;
2629
import java.util.List;
2730

2831
import static java.lang.String.format;
@@ -32,8 +35,12 @@
3235
* A BSON output stream that stores the output in a single, un-pooled byte array.
3336
*/
3437
public class BasicOutputBuffer extends OutputBuffer {
35-
private byte[] buffer;
36-
private int position;
38+
39+
/**
40+
* This ByteBuffer allows us to write ObjectIDs without allocating a temporary array per object, and enables us
41+
* to leverage JVM intrinsics for writing little-endian numeric values.
42+
*/
43+
private ByteBuffer buffer;
3744

3845
/**
3946
* Construct an instance with a default initial byte array size.
@@ -48,7 +55,8 @@ public BasicOutputBuffer() {
4855
* @param initialSize the initial size of the byte array
4956
*/
5057
public BasicOutputBuffer(final int initialSize) {
51-
buffer = new byte[initialSize];
58+
// Allocate heap buffer to ensure we can access underlying array
59+
buffer = ByteBuffer.allocate(initialSize).order(LITTLE_ENDIAN);
5260
}
5361

5462
/**
@@ -58,50 +66,76 @@ public BasicOutputBuffer(final int initialSize) {
5866
* @since 3.3
5967
*/
6068
public byte[] getInternalBuffer() {
61-
return buffer;
69+
return buffer.array();
6270
}
6371

6472
@Override
6573
public void write(final byte[] b) {
74+
writeBytes(b, 0, b.length);
75+
}
76+
77+
@Override
78+
public byte[] toByteArray() {
79+
ensureOpen();
80+
return Arrays.copyOf(buffer.array(), buffer.position());
81+
}
82+
83+
@Override
84+
public void writeInt32(final int value) {
85+
ensureOpen();
86+
ensure(4);
87+
buffer.putInt(value);
88+
}
89+
90+
@Override
91+
public void writeInt32(final int position, final int value) {
92+
ensureOpen();
93+
checkPosition(position, 4);
94+
buffer.putInt(position, value);
95+
}
96+
97+
@Override
98+
public void writeInt64(final long value) {
99+
ensureOpen();
100+
ensure(8);
101+
buffer.putLong(value);
102+
}
103+
104+
@Override
105+
public void writeObjectId(final ObjectId value) {
66106
ensureOpen();
67-
write(b, 0, b.length);
107+
ensure(12);
108+
value.putToByteBuffer(buffer);
68109
}
69110

70111
@Override
71112
public void writeBytes(final byte[] bytes, final int offset, final int length) {
72113
ensureOpen();
73114

74115
ensure(length);
75-
System.arraycopy(bytes, offset, buffer, position, length);
76-
position += length;
116+
buffer.put(bytes, offset, length);
77117
}
78118

79119
@Override
80120
public void writeByte(final int value) {
81121
ensureOpen();
82122

83123
ensure(1);
84-
buffer[position++] = (byte) (0xFF & value);
124+
buffer.put((byte) (0xFF & value));
85125
}
86126

87127
@Override
88128
protected void write(final int absolutePosition, final int value) {
89129
ensureOpen();
130+
checkPosition(absolutePosition, 1);
90131

91-
if (absolutePosition < 0) {
92-
throw new IllegalArgumentException(format("position must be >= 0 but was %d", absolutePosition));
93-
}
94-
if (absolutePosition > position - 1) {
95-
throw new IllegalArgumentException(format("position must be <= %d but was %d", position - 1, absolutePosition));
96-
}
97-
98-
buffer[absolutePosition] = (byte) (0xFF & value);
132+
buffer.put(absolutePosition, (byte) (0xFF & value));
99133
}
100134

101135
@Override
102136
public int getPosition() {
103137
ensureOpen();
104-
return position;
138+
return buffer.position();
105139
}
106140

107141
/**
@@ -110,29 +144,32 @@ public int getPosition() {
110144
@Override
111145
public int getSize() {
112146
ensureOpen();
113-
return position;
147+
return buffer.position();
114148
}
115149

116150
@Override
117151
public int pipe(final OutputStream out) throws IOException {
118152
ensureOpen();
119-
out.write(buffer, 0, position);
120-
return position;
153+
out.write(buffer.array(), 0, buffer.position());
154+
return buffer.position();
121155
}
122156

123157
@Override
124158
public void truncateToPosition(final int newPosition) {
125159
ensureOpen();
126-
if (newPosition > position || newPosition < 0) {
160+
if (newPosition > buffer.position() || newPosition < 0) {
127161
throw new IllegalArgumentException();
128162
}
129-
position = newPosition;
163+
// The cast is required for compatibility with JDK 9+ where ByteBuffer's position method is inherited from Buffer.
164+
((Buffer) buffer).position(newPosition);
130165
}
131166

132167
@Override
133168
public List<ByteBuf> getByteBuffers() {
134169
ensureOpen();
135-
return Arrays.asList(new ByteBufNIO(ByteBuffer.wrap(buffer, 0, position).duplicate().order(LITTLE_ENDIAN)));
170+
// Create a flipped copy of the buffer for reading. Note that ByteBufNIO overwrites the endian-ness.
171+
ByteBuffer flipped = ByteBuffer.wrap(buffer.array(), 0, buffer.position());
172+
return Collections.singletonList(new ByteBufNIO(flipped));
136173
}
137174

138175
@Override
@@ -147,19 +184,32 @@ private void ensureOpen() {
147184
}
148185

149186
private void ensure(final int more) {
150-
int need = position + more;
151-
if (need <= buffer.length) {
187+
int length = buffer.position();
188+
int need = length + more;
189+
if (need <= buffer.capacity()) {
152190
return;
153191
}
154192

155-
int newSize = buffer.length * 2;
193+
int newSize = length * 2;
156194
if (newSize < need) {
157195
newSize = need + 128;
158196
}
159197

160-
byte[] n = new byte[newSize];
161-
System.arraycopy(buffer, 0, n, 0, position);
162-
buffer = n;
198+
ByteBuffer tmp = ByteBuffer.allocate(newSize).order(LITTLE_ENDIAN);
199+
tmp.put(buffer.array(), 0, length); // Avoids covariant call to flip on jdk8
200+
this.buffer = tmp;
163201
}
164202

203+
/**
204+
* Ensures that `absolutePosition` is a valid index in `this.buffer` and there is room to write at
205+
* least `bytesToWrite` bytes.
206+
*/
207+
private void checkPosition(final int absolutePosition, final int bytesToWrite) {
208+
if (absolutePosition < 0) {
209+
throw new IllegalArgumentException(format("position must be >= 0 but was %d", absolutePosition));
210+
}
211+
if (absolutePosition > buffer.position() - bytesToWrite) {
212+
throw new IllegalArgumentException(format("position must be <= %d but was %d", buffer.position() - bytesToWrite, absolutePosition));
213+
}
214+
}
165215
}

0 commit comments

Comments
 (0)