Skip to content

CSHARP-5611: Eliminate the temporary byte array allocation in ObjectId.ToString#1960

Open
BorisDog wants to merge 5 commits intomongodb:mainfrom
BorisDog:csharp5611
Open

CSHARP-5611: Eliminate the temporary byte array allocation in ObjectId.ToString#1960
BorisDog wants to merge 5 commits intomongodb:mainfrom
BorisDog:csharp5611

Conversation

@BorisDog
Copy link
Copy Markdown
Contributor

Copilot AI review requested due to automatic review settings April 21, 2026 21:35
@BorisDog BorisDog requested a review from a team as a code owner April 21, 2026 21:35
@BorisDog BorisDog requested review from ajcvickers and sanych-sun and removed request for ajcvickers April 21, 2026 21:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>) and ObjectId.ToCharSpan(Span<char>), plus a ReadOnlySpan<byte> internal constructor.
  • Update ObjectId.ToString() to use string.Create on modern TFMs, delegating formatting to the span-based writer.
  • Restructure/move ObjectIdTests into the MongoDB.Bson.Tests.ObjectModel namespace 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);
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
FromBytesSpan(bytes.AsSpan(index), out _a, out _b, out _c);
FromBytesSpan(bytes.AsSpan(index, 12), out _a, out _b, out _c);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 440 to +489
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);
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Done.

Comment on lines +496 to +537
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
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor optimization, ignored.

@BorisDog BorisDog added the improvement Optimizations or refactoring (no new features or fixes). label Apr 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Optimizations or refactoring (no new features or fixes).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants