diff --git a/LiteDB.Tests/Document/ObjectId_Tests.cs b/LiteDB.Tests/Document/ObjectId_Tests.cs
index effabcb35..2be32a872 100644
--- a/LiteDB.Tests/Document/ObjectId_Tests.cs
+++ b/LiteDB.Tests/Document/ObjectId_Tests.cs
@@ -1,4 +1,6 @@
-using FluentAssertions;
+using System;
+using FluentAssertions;
+using LiteDB;
using Xunit;
namespace LiteDB.Tests.Document
@@ -41,5 +43,44 @@ public void ObjectId_Equals_Null_Does_Not_Throw()
oid1.Equals(null).Should().BeFalse();
oid1.Equals(oid0).Should().BeFalse();
}
+
+ [Fact]
+ public void ObjectId_ToString_Minimizes_Allocations()
+ {
+ var objectId = ObjectId.NewObjectId();
+
+ objectId.ToString();
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+
+ var before = GC.GetAllocatedBytesForCurrentThread();
+ var hex = objectId.ToString();
+ var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
+
+ hex.Should().HaveLength(24);
+ allocated.Should().BeLessThan(220);
+ }
+
+ [Fact]
+ public void ObjectId_FromHex_Minimizes_Allocations()
+ {
+ var original = ObjectId.NewObjectId();
+ var hex = original.ToString();
+
+ _ = new ObjectId(hex);
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+
+ var before = GC.GetAllocatedBytesForCurrentThread();
+ var parsed = new ObjectId(hex);
+ var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
+
+ parsed.Should().Be(original);
+ allocated.Should().BeLessThan(220);
+ }
}
-}
\ No newline at end of file
+}
diff --git a/LiteDB/Document/ObjectId.cs b/LiteDB/Document/ObjectId.cs
index 84b0af12f..5ea8aac15 100644
--- a/LiteDB/Document/ObjectId.cs
+++ b/LiteDB/Document/ObjectId.cs
@@ -1,4 +1,5 @@
using System;
+using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Security;
@@ -120,22 +121,30 @@ public ObjectId(byte[] bytes, int startIndex = 0)
bytes[startIndex + 11];
}
+ private const int ObjectIdByteLength = 12;
+ private const int ObjectIdStringLength = ObjectIdByteLength * 2;
+
///
/// Convert hex value string in byte array
///
private static byte[] FromHex(string value)
{
if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value));
- if (value.Length != 24) throw new ArgumentException(string.Format("ObjectId strings should be 24 hex characters, got {0} : \"{1}\"", value.Length, value));
+ if (value.Length != ObjectIdStringLength) throw new ArgumentException(string.Format("ObjectId strings should be 24 hex characters, got {0} : \"{1}\"", value.Length, value));
- var bytes = new byte[12];
+ var hex = value.AsSpan();
- for (var i = 0; i < 24; i += 2)
- {
- bytes[i / 2] = Convert.ToByte(value.Substring(i, 2), 16);
- }
+#if NET8_0_OR_GREATER
+ return Convert.FromHexString(hex);
+#else
+ Span buffer = stackalloc byte[ObjectIdByteLength];
+ WriteBytesFromHex(hex, buffer);
- return bytes;
+ var result = new byte[ObjectIdByteLength];
+ buffer.CopyTo(result);
+
+ return result;
+#endif
}
#endregion
@@ -199,34 +208,123 @@ public int CompareTo(ObjectId other)
///
public void ToByteArray(byte[] bytes, int startIndex)
{
- bytes[startIndex + 0] = (byte)(this.Timestamp >> 24);
- bytes[startIndex + 1] = (byte)(this.Timestamp >> 16);
- bytes[startIndex + 2] = (byte)(this.Timestamp >> 8);
- bytes[startIndex + 3] = (byte)(this.Timestamp);
- bytes[startIndex + 4] = (byte)(this.Machine >> 16);
- bytes[startIndex + 5] = (byte)(this.Machine >> 8);
- bytes[startIndex + 6] = (byte)(this.Machine);
- bytes[startIndex + 7] = (byte)(this.Pid >> 8);
- bytes[startIndex + 8] = (byte)(this.Pid);
- bytes[startIndex + 9] = (byte)(this.Increment >> 16);
- bytes[startIndex + 10] = (byte)(this.Increment >> 8);
- bytes[startIndex + 11] = (byte)(this.Increment);
+ this.WriteBytes(bytes.AsSpan(startIndex, ObjectIdByteLength));
}
public byte[] ToByteArray()
{
- var bytes = new byte[12];
+ var bytes = new byte[ObjectIdByteLength];
- this.ToByteArray(bytes, 0);
+ this.WriteBytes(bytes);
return bytes;
}
public override string ToString()
{
- return BitConverter.ToString(this.ToByteArray()).Replace("-", "").ToLower();
+#if NET8_0_OR_GREATER
+ Span buffer = stackalloc byte[ObjectIdByteLength];
+
+ this.WriteBytes(buffer);
+
+ return Convert.ToHexString(buffer).ToLowerInvariant();
+#else
+ Span buffer = stackalloc byte[ObjectIdByteLength];
+
+ this.WriteBytes(buffer);
+
+ var rented = ArrayPool.Shared.Rent(ObjectIdStringLength);
+
+ try
+ {
+ var chars = rented.AsSpan(0, ObjectIdStringLength);
+ WriteHexLower(buffer, chars);
+
+ return new string(rented, 0, ObjectIdStringLength);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(rented);
+ }
+#endif
+ }
+
+ private void WriteBytes(Span destination)
+ {
+ if (destination.Length < ObjectIdByteLength)
+ {
+ throw new ArgumentException("Destination span is too short.", nameof(destination));
+ }
+
+ destination[0] = (byte)(this.Timestamp >> 24);
+ destination[1] = (byte)(this.Timestamp >> 16);
+ destination[2] = (byte)(this.Timestamp >> 8);
+ destination[3] = (byte)(this.Timestamp);
+ destination[4] = (byte)(this.Machine >> 16);
+ destination[5] = (byte)(this.Machine >> 8);
+ destination[6] = (byte)(this.Machine);
+ destination[7] = (byte)(this.Pid >> 8);
+ destination[8] = (byte)(this.Pid);
+ destination[9] = (byte)(this.Increment >> 16);
+ destination[10] = (byte)(this.Increment >> 8);
+ destination[11] = (byte)(this.Increment);
}
+#if !NET8_0_OR_GREATER
+ private static void WriteHexLower(ReadOnlySpan source, Span destination)
+ {
+ if (destination.Length < ObjectIdStringLength)
+ {
+ throw new ArgumentException("Destination span is too short.", nameof(destination));
+ }
+
+ for (var i = 0; i < source.Length; i++)
+ {
+ var value = source[i];
+ destination[i * 2] = GetHexCharacter(value >> 4);
+ destination[i * 2 + 1] = GetHexCharacter(value & 0x0F);
+ }
+ }
+
+ private static char GetHexCharacter(int value)
+ {
+ return (char)(value < 10 ? '0' + value : 'a' + (value - 10));
+ }
+
+ private static void WriteBytesFromHex(ReadOnlySpan hex, Span destination)
+ {
+ if (destination.Length < ObjectIdByteLength)
+ {
+ throw new ArgumentException("Destination span is too short.", nameof(destination));
+ }
+
+ for (var i = 0; i < destination.Length; i++)
+ {
+ var high = ParseHexDigit(hex[i * 2]);
+ var low = ParseHexDigit(hex[i * 2 + 1]);
+
+ destination[i] = (byte)((high << 4) | low);
+ }
+ }
+
+ private static int ParseHexDigit(char c)
+ {
+ if ((uint)(c - '0') <= 9)
+ {
+ return c - '0';
+ }
+
+ var lowered = (char)(c | 0x20);
+
+ if ((uint)(lowered - 'a') <= 5)
+ {
+ return lowered - 'a' + 10;
+ }
+
+ throw new FormatException(string.Format("Invalid hex character '{0}' in ObjectId.", c));
+ }
+#endif
+
#endregion
#region Operators