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