From c36e26d1f6d11c657e8083ecbcbe831b29afa27a Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Wed, 29 Jun 2022 21:41:29 +0300 Subject: [PATCH] Optimize `HashCode.AddBytes` for inputs larger than 16 bytes. (#70095) * Optimize `HashCode.AddBytes` for inputs larger than 16 bytes. * Address PR feedback, refactor control flow and inline UnsafeAddMany. Co-authored-by: Tanner Gooding * Add more asserts before memory reads and move one inside the main loop. * Address PR feedback. Co-authored-by: Stephen Toub Co-authored-by: Tanner Gooding Co-authored-by: Stephen Toub --- .../src/System/HashCode.cs | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/HashCode.cs b/src/libraries/System.Private.CoreLib/src/System/HashCode.cs index 8aeece9a1028d..e11a37af24124 100644 --- a/src/libraries/System.Private.CoreLib/src/System/HashCode.cs +++ b/src/libraries/System.Private.CoreLib/src/System/HashCode.cs @@ -42,6 +42,7 @@ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -319,8 +320,62 @@ 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; + } + + // Usually Add calls Initialize but if we haven't used HashCode before it won't have been called. + if (_length == 0) + { + Initialize(out _v1, out _v2, out _v3, out _v4); + } + else + { + // If we have at least 16 bytes to hash, we can add them in 16-byte batches, + // but we first have to add enough data to flush any queued values. + 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)); + 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)); + 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)); + 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)) + { + 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); + + _length += 4; + pos = ref Unsafe.Add(ref pos, sizeof(int) * 4); + } + + Small: // Add four bytes at a time until the input has fewer than four bytes remaining. - while ((nint)Unsafe.ByteOffset(ref pos, ref end) >= sizeof(int)) + while (Unsafe.ByteOffset(ref pos, ref end) >= sizeof(int)) { Add(Unsafe.ReadUnaligned(ref pos)); pos = ref Unsafe.Add(ref pos, sizeof(int));