From 3f3d58fc6763e3df5df7815bd73699215e5fe11e Mon Sep 17 00:00:00 2001 From: int21h Date: Mon, 16 Sep 2024 12:02:03 -0400 Subject: [PATCH 1/3] Remove memory allocations in GuidCombGenerator under .NET 8+ --- .../IdGen/GuidComb/GuidCombFixture.cs | 32 ++++++++++++++ src/NHibernate/Id/GuidCombGenerator.cs | 43 ++++++++++++++++--- 2 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 src/NHibernate.Test/IdGen/GuidComb/GuidCombFixture.cs diff --git a/src/NHibernate.Test/IdGen/GuidComb/GuidCombFixture.cs b/src/NHibernate.Test/IdGen/GuidComb/GuidCombFixture.cs new file mode 100644 index 00000000000..b79bbb807b1 --- /dev/null +++ b/src/NHibernate.Test/IdGen/GuidComb/GuidCombFixture.cs @@ -0,0 +1,32 @@ +using System; +using NHibernate.Id; +using NUnit.Framework; + +namespace NHibernate.Test.IdGen.GuidComb; + +[TestFixture] +public class GuidCombFixture +{ + class GuidCombGeneratorEx : GuidCombGenerator + { + public static Guid Generate(string guid, DateTime utcNow) => GenerateComb(Guid.Parse(guid), utcNow); + } + + [Test] + public void CanGenerateSequentialGuid() + { + Assert.AreEqual(Guid.Parse("076a04fa-ef4e-4093-8479-b0e10103cdc5"), + GuidCombGeneratorEx.Generate( + "076a04fa-ef4e-4093-8479-8599e96f14cf", + new DateTime(2023, 12, 23, 15, 45, 55, DateTimeKind.Utc)), + "seed: 076a04fa"); + + Assert.AreEqual(Guid.Parse("81162ee2-a4cb-4611-9327-d61f0137e5b6"), + GuidCombGeneratorEx.Generate( + "81162ee2-a4cb-4611-9327-23bbda36176c", + new DateTime(2050, 01, 29, 18, 55, 35, DateTimeKind.Utc)), + "seed: 81162ee2"); + + } + +} diff --git a/src/NHibernate/Id/GuidCombGenerator.cs b/src/NHibernate/Id/GuidCombGenerator.cs index cb01227c979..076ef8bc1cc 100644 --- a/src/NHibernate/Id/GuidCombGenerator.cs +++ b/src/NHibernate/Id/GuidCombGenerator.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using NHibernate.Engine; namespace NHibernate.Id @@ -36,21 +37,50 @@ public partial class GuidCombGenerator : IIdentifierGenerator /// The new identifier as a . public object Generate(ISessionImplementor session, object obj) { - return GenerateComb(); + return GenerateComb(Guid.NewGuid(), DateTime.UtcNow); } /// /// Generate a new using the comb algorithm. /// - private Guid GenerateComb() + protected static Guid GenerateComb(in Guid guid, DateTime utcNow) { - byte[] guidArray = Guid.NewGuid().ToByteArray(); +#if NET8_0_OR_GREATER + Span guidArray = stackalloc byte[16]; + Span msecsArray = stackalloc byte[sizeof(long)]; + Span daysArray = stackalloc byte[sizeof(int)]; - DateTime now = DateTime.UtcNow; + var bytesWritten = guid.TryWriteBytes(guidArray); + Debug.Assert(bytesWritten); // Get the days and milliseconds which will be used to build the byte string - TimeSpan days = new TimeSpan(now.Ticks - BaseDateTicks); - TimeSpan msecs = now.TimeOfDay; + TimeSpan days = new TimeSpan(utcNow.Ticks - BaseDateTicks); + TimeSpan msecs = utcNow.TimeOfDay; + + // Convert to a byte array + // Note that SQL Server is accurate to 1/300th of a millisecond so we divide by 3.333333 + + bytesWritten = BitConverter.TryWriteBytes(daysArray, days.Days) + && BitConverter.TryWriteBytes(msecsArray, (long)(msecs.TotalMilliseconds / 3.333333)); + Debug.Assert(bytesWritten); + + msecsArray.Reverse(); + + // Copy the bytes into the guid + //Array.Copy(daysArray, daysArray.Length - 2, guidArray, guidArray.Length - 6, 2); + guidArray[10] = daysArray[1]; + guidArray[11] = daysArray[0]; + + //Array.Copy(msecsArray, msecsArray.Length - 4, guidArray, guidArray.Length - 4, 4); + msecsArray[^4..].CopyTo(guidArray[^4..]); + return new Guid(guidArray); +#else + + byte[] guidArray = guid.ToByteArray(); + + // Get the days and milliseconds which will be used to build the byte string + TimeSpan days = new TimeSpan(utcNow.Ticks - BaseDateTicks); + TimeSpan msecs = utcNow.TimeOfDay; // Convert to a byte array // Note that SQL Server is accurate to 1/300th of a millisecond so we divide by 3.333333 @@ -66,6 +96,7 @@ private Guid GenerateComb() Array.Copy(msecsArray, msecsArray.Length - 4, guidArray, guidArray.Length - 4, 4); return new Guid(guidArray); +#endif } #endregion From 9b5714a86ea0bff5d109817e4561b017111b784f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 25 Sep 2024 14:26:34 +0000 Subject: [PATCH 2/3] Generate async files --- src/NHibernate/Async/Id/GuidCombGenerator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NHibernate/Async/Id/GuidCombGenerator.cs b/src/NHibernate/Async/Id/GuidCombGenerator.cs index 59540f3d2ca..eb4ef6c7c63 100644 --- a/src/NHibernate/Async/Id/GuidCombGenerator.cs +++ b/src/NHibernate/Async/Id/GuidCombGenerator.cs @@ -9,6 +9,7 @@ using System; +using System.Diagnostics; using NHibernate.Engine; namespace NHibernate.Id From b19ca2809c92d23ca6f1b5453e83ceb855eb42d0 Mon Sep 17 00:00:00 2001 From: Alex Zaytsev Date: Tue, 11 Feb 2025 10:51:43 +1000 Subject: [PATCH 3/3] apply suggestion from comments --- src/NHibernate/Id/GuidCombGenerator.cs | 62 +++++++------------------- 1 file changed, 15 insertions(+), 47 deletions(-) diff --git a/src/NHibernate/Id/GuidCombGenerator.cs b/src/NHibernate/Id/GuidCombGenerator.cs index 076ef8bc1cc..adbbd78196d 100644 --- a/src/NHibernate/Id/GuidCombGenerator.cs +++ b/src/NHibernate/Id/GuidCombGenerator.cs @@ -43,60 +43,28 @@ public object Generate(ISessionImplementor session, object obj) /// /// Generate a new using the comb algorithm. /// - protected static Guid GenerateComb(in Guid guid, DateTime utcNow) + protected static Guid GenerateComb(Guid guid, DateTime utcNow) { -#if NET8_0_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER Span guidArray = stackalloc byte[16]; - Span msecsArray = stackalloc byte[sizeof(long)]; - Span daysArray = stackalloc byte[sizeof(int)]; - - var bytesWritten = guid.TryWriteBytes(guidArray); - Debug.Assert(bytesWritten); - - // Get the days and milliseconds which will be used to build the byte string - TimeSpan days = new TimeSpan(utcNow.Ticks - BaseDateTicks); - TimeSpan msecs = utcNow.TimeOfDay; - - // Convert to a byte array - // Note that SQL Server is accurate to 1/300th of a millisecond so we divide by 3.333333 - - bytesWritten = BitConverter.TryWriteBytes(daysArray, days.Days) - && BitConverter.TryWriteBytes(msecsArray, (long)(msecs.TotalMilliseconds / 3.333333)); - Debug.Assert(bytesWritten); - - msecsArray.Reverse(); - - // Copy the bytes into the guid - //Array.Copy(daysArray, daysArray.Length - 2, guidArray, guidArray.Length - 6, 2); - guidArray[10] = daysArray[1]; - guidArray[11] = daysArray[0]; - - //Array.Copy(msecsArray, msecsArray.Length - 4, guidArray, guidArray.Length - 4, 4); - msecsArray[^4..].CopyTo(guidArray[^4..]); - return new Guid(guidArray); + guid.TryWriteBytes(guidArray); #else - - byte[] guidArray = guid.ToByteArray(); - + var guidArray = guid.ToByteArray(); +#endif // Get the days and milliseconds which will be used to build the byte string - TimeSpan days = new TimeSpan(utcNow.Ticks - BaseDateTicks); - TimeSpan msecs = utcNow.TimeOfDay; - - // Convert to a byte array + var ts = new TimeSpan(utcNow.Ticks - BaseDateTicks); + var days = ts.Days; + guidArray[10] = (byte) (days >> 8); + guidArray[11] = (byte) days; + // Note that SQL Server is accurate to 1/300th of a millisecond so we divide by 3.333333 - byte[] daysArray = BitConverter.GetBytes(days.Days); - byte[] msecsArray = BitConverter.GetBytes((long) (msecs.TotalMilliseconds / 3.333333)); - - // Reverse the bytes to match SQL Servers ordering - Array.Reverse(daysArray); - Array.Reverse(msecsArray); - - // Copy the bytes into the guid - Array.Copy(daysArray, daysArray.Length - 2, guidArray, guidArray.Length - 6, 2); - Array.Copy(msecsArray, msecsArray.Length - 4, guidArray, guidArray.Length - 4, 4); + var msecs = (long) (utcNow.TimeOfDay.TotalMilliseconds / 3.333333); + guidArray[12] = (byte) (msecs >> 24); + guidArray[13] = (byte) (msecs >> 16); + guidArray[14] = (byte) (msecs >> 8); + guidArray[15] = (byte) msecs; return new Guid(guidArray); -#endif } #endregion