CSHARP-5611: Eliminate the temporary byte array allocation in ObjectId.ToString#1960
CSHARP-5611: Eliminate the temporary byte array allocation in ObjectId.ToString#1960BorisDog wants to merge 5 commits intomongodb:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Optimizes MongoDB.Bson.ObjectId string/byte conversions (CSHARP-5611) by introducing span-based formatting helpers and using string.Create where available to reduce intermediate allocations, along with test updates/coverage for the new APIs.
Changes:
- Add
ObjectId.ToByteSpan(Span<byte>)andObjectId.ToCharSpan(Span<char>), plus aReadOnlySpan<byte>internal constructor. - Update
ObjectId.ToString()to usestring.Createon modern TFMs, delegating formatting to the span-based writer. - Restructure/move
ObjectIdTestsinto theMongoDB.Bson.Tests.ObjectModelnamespace and update dependent tests’ usings.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| tests/MongoDB.Bson.Tests/ObjectModel/ObjectIdTests.cs | Adds tests for new span-based APIs and aligns namespace with other ObjectModel tests. |
| tests/MongoDB.Bson.Tests/IO/ByteBufferStreamTests.cs | Imports new test namespace to keep using ObjectIdReflector. |
| tests/MongoDB.Bson.Tests/IO/BsonStreamAdapterTests.cs | Imports new test namespace to keep using ObjectIdReflector. |
| src/MongoDB.Bson/ObjectModel/ObjectId.cs | Adds span-based conversion APIs and updates ToString() to use string.Create on supported TFMs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| internal ObjectId(byte[] bytes, int index) | ||
| { | ||
| FromByteArray(bytes, index, out _a, out _b, out _c); | ||
| FromBytesSpan(bytes.AsSpan(index), out _a, out _b, out _c); |
There was a problem hiding this comment.
In the internal ObjectId(byte[] bytes, int index) constructor, bytes.AsSpan(index) passes the entire remainder of the array to FromBytesSpan, even though only 12 bytes are required. Slicing to exactly 12 bytes (and optionally validating that at least 12 bytes remain) would make the contract clearer and can help avoid accidental out-of-range reads if this constructor is ever reused with a shorter trailing segment.
| FromBytesSpan(bytes.AsSpan(index), out _a, out _b, out _c); | |
| FromBytesSpan(bytes.AsSpan(index, 12), out _a, out _b, out _c); |
| public void ToByteArray(byte[] destination, int offset) | ||
| { | ||
| if (destination == null) | ||
| { | ||
| throw new ArgumentNullException("destination"); | ||
| throw new ArgumentNullException(nameof(destination)); | ||
| } | ||
| if (offset + 12 > destination.Length) | ||
| { | ||
| throw new ArgumentException("Not enough room in destination buffer.", "offset"); | ||
| throw new ArgumentException("Not enough room in destination buffer.", nameof(offset)); | ||
| } | ||
|
|
||
| destination[offset + 0] = (byte)(_a >> 24); | ||
| destination[offset + 1] = (byte)(_a >> 16); | ||
| destination[offset + 2] = (byte)(_a >> 8); | ||
| destination[offset + 3] = (byte)(_a); | ||
| destination[offset + 4] = (byte)(_b >> 24); | ||
| destination[offset + 5] = (byte)(_b >> 16); | ||
| destination[offset + 6] = (byte)(_b >> 8); | ||
| destination[offset + 7] = (byte)(_b); | ||
| destination[offset + 8] = (byte)(_c >> 24); | ||
| destination[offset + 9] = (byte)(_c >> 16); | ||
| destination[offset + 10] = (byte)(_c >> 8); | ||
| destination[offset + 11] = (byte)(_c); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Converts the ObjectId to a byte buffer provided by the input span. | ||
| /// </summary> | ||
| /// <param name="destination">The destination byte span.</param> | ||
| /// <exception cref="ArgumentException">Not enough room in destination span.</exception> | ||
| public void ToByteSpan(Span<byte> destination) | ||
| { | ||
| if (destination.Length < 12) | ||
| { | ||
| throw new ArgumentException("Not enough room in destination span.", nameof(destination)); | ||
| } | ||
|
|
||
| destination[0] = (byte)(_a >> 24); | ||
| destination[1] = (byte)(_a >> 16); | ||
| destination[2] = (byte)(_a >> 8); | ||
| destination[3] = (byte)(_a); | ||
| destination[4] = (byte)(_b >> 24); | ||
| destination[5] = (byte)(_b >> 16); | ||
| destination[6] = (byte)(_b >> 8); | ||
| destination[7] = (byte)(_b); | ||
| destination[8] = (byte)(_c >> 24); | ||
| destination[9] = (byte)(_c >> 16); | ||
| destination[10] = (byte)(_c >> 8); | ||
| destination[11] = (byte)(_c); | ||
| } |
There was a problem hiding this comment.
ToByteArray(byte[] destination, int offset) and ToByteSpan(Span<byte> destination) duplicate the same 12 byte writes. Consider factoring the write logic into a single span-based helper (e.g., write to destination.AsSpan(offset, 12) after validating bounds) so changes/fixes only need to be made in one place.
| public void ToCharSpan(Span<char> destination) | ||
| { | ||
| if (destination.Length < 24) | ||
| { | ||
| throw new ArgumentException("Not enough room in destination span.", nameof(destination)); | ||
| } | ||
|
|
||
| destination[0] = BsonUtils.ToHexChar((_a >> 28) & 0x0f); | ||
| destination[1] = BsonUtils.ToHexChar((_a >> 24) & 0x0f); | ||
| destination[2] = BsonUtils.ToHexChar((_a >> 20) & 0x0f); | ||
| destination[3] = BsonUtils.ToHexChar((_a >> 16) & 0x0f); | ||
| destination[4] = BsonUtils.ToHexChar((_a >> 12) & 0x0f); | ||
| destination[5] = BsonUtils.ToHexChar((_a >> 8) & 0x0f); | ||
| destination[6] = BsonUtils.ToHexChar((_a >> 4) & 0x0f); | ||
| destination[7] = BsonUtils.ToHexChar(_a & 0x0f); | ||
| destination[8] = BsonUtils.ToHexChar((_b >> 28) & 0x0f); | ||
| destination[9] = BsonUtils.ToHexChar((_b >> 24) & 0x0f); | ||
| destination[10] = BsonUtils.ToHexChar((_b >> 20) & 0x0f); | ||
| destination[11] = BsonUtils.ToHexChar((_b >> 16) & 0x0f); | ||
| destination[12] = BsonUtils.ToHexChar((_b >> 12) & 0x0f); | ||
| destination[13] = BsonUtils.ToHexChar((_b >> 8) & 0x0f); | ||
| destination[14] = BsonUtils.ToHexChar((_b >> 4) & 0x0f); | ||
| destination[15] = BsonUtils.ToHexChar(_b & 0x0f); | ||
| destination[16] = BsonUtils.ToHexChar((_c >> 28) & 0x0f); | ||
| destination[17] = BsonUtils.ToHexChar((_c >> 24) & 0x0f); | ||
| destination[18] = BsonUtils.ToHexChar((_c >> 20) & 0x0f); | ||
| destination[19] = BsonUtils.ToHexChar((_c >> 16) & 0x0f); | ||
| destination[20] = BsonUtils.ToHexChar((_c >> 12) & 0x0f); | ||
| destination[21] = BsonUtils.ToHexChar((_c >> 8) & 0x0f); | ||
| destination[22] = BsonUtils.ToHexChar((_c >> 4) & 0x0f); | ||
| destination[23] = BsonUtils.ToHexChar(_c & 0x0f); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns a string representation of the value. | ||
| /// </summary> | ||
| /// <returns>A string representation of the value.</returns> | ||
| public override string ToString() | ||
| { | ||
| #if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER | ||
| return string.Create(24, this, static (span, input) => input.ToCharSpan(span)); | ||
| #else |
There was a problem hiding this comment.
ToString() now calls ToCharSpan, which performs a destination-length check even though string.Create(24, ...) always provides a 24-char span. If the goal is maximizing ToString performance, consider splitting into an unchecked private writer used by ToString and a checked public ToCharSpan, avoiding the redundant branch on the hot path.
There was a problem hiding this comment.
Minor optimization, ignored.
CSHARP-5611