From 49ad61f84fca5f235f84b4ac66069febd2762bcb Mon Sep 17 00:00:00 2001 From: EgorBo Date: Fri, 24 Apr 2026 14:15:50 +0200 Subject: [PATCH 1/2] Use safe Span.Slice loop pattern in HashCode.AddBytes Rewrites `HashCode.AddBytes` to use the safe `while (value.Length >= CONST) { ...; value = value.Slice(CONST); }` pattern, replacing `Unsafe.ReadUnaligned` / `Unsafe.Add(ref, off)` / `Unsafe.IsAddressLessThan` based pointer walking with `MemoryMarshal.Read` and `ReadOnlySpan.Slice` over the input span. The JIT recognizes this exact loop shape and elides the bounds checks, so the resulting codegen is equivalent to the previous unsafe form. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/HashCode.cs | 51 ++++++++----------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/HashCode.cs b/src/libraries/System.Private.CoreLib/src/System/HashCode.cs index e11a37af24124a..e41052e6166156 100644 --- a/src/libraries/System.Private.CoreLib/src/System/HashCode.cs +++ b/src/libraries/System.Private.CoreLib/src/System/HashCode.cs @@ -317,9 +317,6 @@ public void Add(T value, IEqualityComparer? comparer) /// public void AddBytes(ReadOnlySpan value) { - ref byte pos = ref MemoryMarshal.GetReference(value); - ref byte end = ref Unsafe.Add(ref pos, value.Length); - if (value.Length < (sizeof(int) * 4)) { goto Small; @@ -337,55 +334,47 @@ public void AddBytes(ReadOnlySpan value) switch (_length % 4) { case 1: - Debug.Assert(Unsafe.ByteOffset(ref pos, ref end) >= sizeof(int)); - Add(Unsafe.ReadUnaligned(ref pos)); - pos = ref Unsafe.Add(ref pos, sizeof(int)); + Debug.Assert(value.Length >= sizeof(int)); + Add(MemoryMarshal.Read(value)); + value = value.Slice(sizeof(int)); goto case 2; case 2: - Debug.Assert(Unsafe.ByteOffset(ref pos, ref end) >= sizeof(int)); - Add(Unsafe.ReadUnaligned(ref pos)); - pos = ref Unsafe.Add(ref pos, sizeof(int)); + Debug.Assert(value.Length >= sizeof(int)); + Add(MemoryMarshal.Read(value)); + value = value.Slice(sizeof(int)); goto case 3; case 3: - Debug.Assert(Unsafe.ByteOffset(ref pos, ref end) >= sizeof(int)); - Add(Unsafe.ReadUnaligned(ref pos)); - pos = ref Unsafe.Add(ref pos, sizeof(int)); + Debug.Assert(value.Length >= sizeof(int)); + Add(MemoryMarshal.Read(value)); + value = value.Slice(sizeof(int)); break; } } // With the queue clear, we add sixteen bytes at a time until the input has fewer than sixteen bytes remaining. - // We first have to round the end pointer to the nearest 16-byte block from the offset. This makes the loop's condition simpler. - ref byte blockEnd = ref Unsafe.Subtract(ref end, Unsafe.ByteOffset(ref pos, ref end) % (sizeof(int) * 4)); - while (Unsafe.IsAddressLessThan(ref pos, ref blockEnd)) + while (value.Length >= sizeof(int) * 4) { - Debug.Assert(Unsafe.ByteOffset(ref pos, ref blockEnd) >= (sizeof(int) * 4)); - uint v1 = Unsafe.ReadUnaligned(ref pos); - _v1 = Round(_v1, v1); - uint v2 = Unsafe.ReadUnaligned(ref Unsafe.Add(ref pos, sizeof(int) * 1)); - _v2 = Round(_v2, v2); - uint v3 = Unsafe.ReadUnaligned(ref Unsafe.Add(ref pos, sizeof(int) * 2)); - _v3 = Round(_v3, v3); - uint v4 = Unsafe.ReadUnaligned(ref Unsafe.Add(ref pos, sizeof(int) * 3)); - _v4 = Round(_v4, v4); + _v1 = Round(_v1, MemoryMarshal.Read(value)); + _v2 = Round(_v2, MemoryMarshal.Read(value.Slice(sizeof(int) * 1))); + _v3 = Round(_v3, MemoryMarshal.Read(value.Slice(sizeof(int) * 2))); + _v4 = Round(_v4, MemoryMarshal.Read(value.Slice(sizeof(int) * 3))); _length += 4; - pos = ref Unsafe.Add(ref pos, sizeof(int) * 4); + value = value.Slice(sizeof(int) * 4); } Small: // Add four bytes at a time until the input has fewer than four bytes remaining. - while (Unsafe.ByteOffset(ref pos, ref end) >= sizeof(int)) + while (value.Length >= sizeof(int)) { - Add(Unsafe.ReadUnaligned(ref pos)); - pos = ref Unsafe.Add(ref pos, sizeof(int)); + Add(MemoryMarshal.Read(value)); + value = value.Slice(sizeof(int)); } // Add the remaining bytes a single byte at a time. - while (Unsafe.IsAddressLessThan(ref pos, ref end)) + foreach (byte b in value) { - Add((int)pos); - pos = ref Unsafe.Add(ref pos, 1); + Add((int)b); } } From c575920ea73ed23a037c973a8ac726aa0b38def7 Mon Sep 17 00:00:00 2001 From: EgorBo Date: Fri, 24 Apr 2026 14:28:50 +0200 Subject: [PATCH 2/2] Use BitConverter.ToInt32/ToUInt32 instead of MemoryMarshal.Read Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/HashCode.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/HashCode.cs b/src/libraries/System.Private.CoreLib/src/System/HashCode.cs index e41052e6166156..6a80fa07da246f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/HashCode.cs +++ b/src/libraries/System.Private.CoreLib/src/System/HashCode.cs @@ -45,7 +45,6 @@ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; #pragma warning disable CA1066 // Implement IEquatable when overriding Object.Equals @@ -335,17 +334,17 @@ public void AddBytes(ReadOnlySpan value) { case 1: Debug.Assert(value.Length >= sizeof(int)); - Add(MemoryMarshal.Read(value)); + Add(BitConverter.ToInt32(value)); value = value.Slice(sizeof(int)); goto case 2; case 2: Debug.Assert(value.Length >= sizeof(int)); - Add(MemoryMarshal.Read(value)); + Add(BitConverter.ToInt32(value)); value = value.Slice(sizeof(int)); goto case 3; case 3: Debug.Assert(value.Length >= sizeof(int)); - Add(MemoryMarshal.Read(value)); + Add(BitConverter.ToInt32(value)); value = value.Slice(sizeof(int)); break; } @@ -354,10 +353,10 @@ public void AddBytes(ReadOnlySpan value) // With the queue clear, we add sixteen bytes at a time until the input has fewer than sixteen bytes remaining. while (value.Length >= sizeof(int) * 4) { - _v1 = Round(_v1, MemoryMarshal.Read(value)); - _v2 = Round(_v2, MemoryMarshal.Read(value.Slice(sizeof(int) * 1))); - _v3 = Round(_v3, MemoryMarshal.Read(value.Slice(sizeof(int) * 2))); - _v4 = Round(_v4, MemoryMarshal.Read(value.Slice(sizeof(int) * 3))); + _v1 = Round(_v1, BitConverter.ToUInt32(value)); + _v2 = Round(_v2, BitConverter.ToUInt32(value.Slice(sizeof(int) * 1))); + _v3 = Round(_v3, BitConverter.ToUInt32(value.Slice(sizeof(int) * 2))); + _v4 = Round(_v4, BitConverter.ToUInt32(value.Slice(sizeof(int) * 3))); _length += 4; value = value.Slice(sizeof(int) * 4); @@ -367,7 +366,7 @@ public void AddBytes(ReadOnlySpan value) // Add four bytes at a time until the input has fewer than four bytes remaining. while (value.Length >= sizeof(int)) { - Add(MemoryMarshal.Read(value)); + Add(BitConverter.ToInt32(value)); value = value.Slice(sizeof(int)); }