From d50d542a49783b3a540660e8361bf04c309b4fb8 Mon Sep 17 00:00:00 2001 From: EgorBo Date: Thu, 23 Feb 2023 12:45:34 +0100 Subject: [PATCH 01/11] Vectorize Convert.FromHexString --- THIRD-PARTY-NOTICES.TXT | 30 +++++ .../Common/src/System/HexConverter.cs | 110 ++++++++++++++++++ .../tests/System/Convert.FromHexString.cs | 14 +++ 3 files changed, 154 insertions(+) diff --git a/THIRD-PARTY-NOTICES.TXT b/THIRD-PARTY-NOTICES.TXT index feb4d4fe851c8c..f60a240e7ee207 100644 --- a/THIRD-PARTY-NOTICES.TXT +++ b/THIRD-PARTY-NOTICES.TXT @@ -374,6 +374,36 @@ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +License notice for vectorized hex parsing +-------------------------------------------------------- + +Copyright (c) 2022, Geoff Langdale +Copyright (c) 2022, Wojciech Mula +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + License notice for RFC 3492 --------------------------- diff --git a/src/libraries/Common/src/System/HexConverter.cs b/src/libraries/Common/src/System/HexConverter.cs index 7986ccb43a9b1f..dbad19754a0a2a 100644 --- a/src/libraries/Common/src/System/HexConverter.cs +++ b/src/libraries/Common/src/System/HexConverter.cs @@ -4,10 +4,12 @@ using System.Diagnostics; using System.Runtime.CompilerServices; #if SYSTEM_PRIVATE_CORELIB +using System.Buffers.Binary; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.Arm; using System.Runtime.Intrinsics.X86; +using System.Text.Unicode; #endif namespace System @@ -228,9 +230,117 @@ public static char ToCharLower(int value) public static bool TryDecodeFromUtf16(ReadOnlySpan chars, Span bytes) { + Debug.Assert(chars.Length <= checked(bytes.Length * 2)); + +#if SYSTEM_PRIVATE_CORELIB + if (BitConverter.IsLittleEndian && (Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && + chars.Length >= Vector128.Count * 2) + { + return TryDecodeFromUtf16_Vector128(chars, bytes); + } +#endif return TryDecodeFromUtf16(chars, bytes, out _); } +#if SYSTEM_PRIVATE_CORELIB + public static bool TryDecodeFromUtf16_Vector128(ReadOnlySpan chars, Span bytes) + { + Debug.Assert(Ssse3.IsSupported || AdvSimd.Arm64.IsSupported); + Debug.Assert(chars.Length <= bytes.Length * 2); + Debug.Assert(chars.Length % 2 == 0); + Debug.Assert(chars.Length >= Vector128.Count * 2); + + nuint offset = 0; + nuint lengthSubVector128 = (nuint)chars.Length - ((nuint)Vector128.Count * 2); + + ref ushort srcRef = ref Unsafe.As(ref MemoryMarshal.GetReference(chars)); + ref byte destRef = ref MemoryMarshal.GetReference(bytes); + + do + { + // The algorithm is UTF8 so we'll be loading two UTF-16 vectors to narrow them into a + // single UTF8 ASCII vector - the implementation can be shared with UTF8 paths. + Vector128 vec1 = Vector128.LoadUnsafe(ref srcRef, offset); + Vector128 vec2 = Vector128.LoadUnsafe(ref srcRef, offset + (nuint)Vector128.Count); + + if (!Utf16Utility.AllCharsInVector128AreAscii(vec1 | vec2)) + { + // Invalid input + break; + } + + Vector128 vec = Vector128.Narrow(vec1, vec2); + + // Based on "Algorithm #3" https://github.com/WojciechMula/toys/blob/master/simd-parse-hex/geoff_algorithm.cpp + // by Geoff Langdale and Wojciech Mula + + // Move digits '0'..'9' into range 0xf6..0xff. + Vector128 t1 = vec + Vector128.Create((byte)(0xFF - '9')); + + // And then correct the range to 0xf0..0xf9. All other bytes become less than 0xf0. + Vector128 t2 = SubtractSaturate(t1, Vector128.Create((byte)6)); + + // Convert into uppercase 'a'..'f' => 'A'..'F' and move hex letter 'A'..'F' into range 0..5. + Vector128 t3 = (vec & Vector128.Create((byte)0xDF)) - Vector128.Create((byte)'A'); + + // And correct the range into 10..15. The non-hex letters bytes become greater than 0x0f. + Vector128 t4 = AddSaturate(t3, Vector128.Create((byte)10)); + + // Convert '0'..'9' into nibbles 0..9. Non-digit bytes become greater than 0x0f. + // Finally choose the result: either valid nibble (0..9/10..15) or some byte greater than 0x0f. + Vector128 nibbles = Vector128.Min(t2 - Vector128.Create((byte)0xF0), t4); + + // Any high bit is a sign that input is not a valid hex data + if (AddSaturate(nibbles, Vector128.Create((byte)(127 - 15))).ExtractMostSignificantBits() != 0) + { + // Invalid input + break; + } + + Vector128 output; + if (Ssse3.IsSupported) + { + output = Ssse3.MultiplyAddAdjacent(nibbles, Vector128.Create((short)0x0110).AsSByte()).AsByte(); + } + else + { + // MultiplyAddAdjacent doesn't exist on ARM + Vector128 evens = + AdvSimd.ShiftLeftLogicalWideningLower(AdvSimd.Arm64.UnzipEven(nibbles, Vector128.Create((byte)1)).GetLower(), 6); + Vector128 odds = AdvSimd.Arm64.TransposeOdd(nibbles, Vector128.Zero).AsUInt16(); + output = Vector128.Add(evens, odds).AsByte(); + } + + // Accumulate output in lower INT64 half and take care about endianness + output = Vector128.Shuffle(output, Vector128.Create((byte)0, 2, 4, 6, 8, 10, 12, 14, 0, 0, 0, 0, 0, 0, 0, 0)); + + // Store 8 bytes in dest by given offset + Unsafe.WriteUnaligned(ref Unsafe.Add(ref destRef, offset / 2), output.AsUInt64().ToScalar()); + + offset += (nuint)Vector128.Count * 2; + if (offset == (nuint)chars.Length) + { + return true; + } + + // Overlap with the current chunk for trailing elements + if (offset > lengthSubVector128) + { + offset = lengthSubVector128; + } + } while (true); + + // Non-ASCII data detected - fall back to the scalar routine. + return TryDecodeFromUtf16(chars.Slice((int)offset), bytes.Slice((int)offset / 2), out _); + + // Remove once these are exposed as cross-plat helpers + static Vector128 AddSaturate(Vector128 a, Vector128 b) => + Sse2.IsSupported ? Sse2.AddSaturate(a, b) : AdvSimd.AddSaturate(a, b); + static Vector128 SubtractSaturate(Vector128 a, Vector128 b) => + Sse2.IsSupported ? Sse2.SubtractSaturate(a, b) : AdvSimd.SubtractSaturate(a, b); + } +#endif + public static bool TryDecodeFromUtf16(ReadOnlySpan chars, Span bytes, out int charsProcessed) { Debug.Assert(chars.Length % 2 == 0, "Un-even number of characters provided"); diff --git a/src/libraries/System.Runtime.Extensions/tests/System/Convert.FromHexString.cs b/src/libraries/System.Runtime.Extensions/tests/System/Convert.FromHexString.cs index da7a83aab1c2d9..719af3ad04cd6c 100644 --- a/src/libraries/System.Runtime.Extensions/tests/System/Convert.FromHexString.cs +++ b/src/libraries/System.Runtime.Extensions/tests/System/Convert.FromHexString.cs @@ -102,5 +102,19 @@ public static void ZeroLength() { Assert.Same(Array.Empty(), Convert.FromHexString(string.Empty)); } + + [Fact] + public static void ToHexFromHexRoundtrip() + { + for (int i = 1; i < 50; i++) + { + byte[] data = System.Security.Cryptography.RandomNumberGenerator.GetBytes(i); + string hex = Convert.ToHexString(data); + Assert.Equal(data, Convert.FromHexString(hex.ToLowerInvariant())); + Assert.Equal(data, Convert.FromHexString(hex.ToUpperInvariant())); + Assert.Throws(() => Convert.FromHexString(hex + " ")); + Assert.Throws(() => Convert.FromHexString("\uAAAA" + hex)); + } + } } } From c86a880c182590e95c03b6a1284f571f4e0849f2 Mon Sep 17 00:00:00 2001 From: EgorBo Date: Thu, 23 Feb 2023 14:15:53 +0100 Subject: [PATCH 02/11] Fix arm64, clean up --- .../Common/src/System/HexConverter.cs | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/libraries/Common/src/System/HexConverter.cs b/src/libraries/Common/src/System/HexConverter.cs index dbad19754a0a2a..32b8809a558be8 100644 --- a/src/libraries/Common/src/System/HexConverter.cs +++ b/src/libraries/Common/src/System/HexConverter.cs @@ -1,3 +1,5 @@ +#define SYSTEM_PRIVATE_CORELIB + // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. @@ -251,7 +253,7 @@ public static bool TryDecodeFromUtf16_Vector128(ReadOnlySpan chars, Span= Vector128.Count * 2); nuint offset = 0; - nuint lengthSubVector128 = (nuint)chars.Length - ((nuint)Vector128.Count * 2); + nuint lengthSubTwoVector128 = (nuint)chars.Length - ((nuint)Vector128.Count * 2); ref ushort srcRef = ref Unsafe.As(ref MemoryMarshal.GetReference(chars)); ref byte destRef = ref MemoryMarshal.GetReference(bytes); @@ -262,58 +264,55 @@ public static bool TryDecodeFromUtf16_Vector128(ReadOnlySpan chars, Span vec1 = Vector128.LoadUnsafe(ref srcRef, offset); Vector128 vec2 = Vector128.LoadUnsafe(ref srcRef, offset + (nuint)Vector128.Count); - if (!Utf16Utility.AllCharsInVector128AreAscii(vec1 | vec2)) { // Invalid input break; } - Vector128 vec = Vector128.Narrow(vec1, vec2); // Based on "Algorithm #3" https://github.com/WojciechMula/toys/blob/master/simd-parse-hex/geoff_algorithm.cpp // by Geoff Langdale and Wojciech Mula - // Move digits '0'..'9' into range 0xf6..0xff. Vector128 t1 = vec + Vector128.Create((byte)(0xFF - '9')); - - // And then correct the range to 0xf0..0xf9. All other bytes become less than 0xf0. + // And then correct the range to 0xf0..0xf9. + // All other bytes become less than 0xf0. Vector128 t2 = SubtractSaturate(t1, Vector128.Create((byte)6)); - - // Convert into uppercase 'a'..'f' => 'A'..'F' and move hex letter 'A'..'F' into range 0..5. + // Convert into uppercase 'a'..'f' => 'A'..'F' and + // move hex letter 'A'..'F' into range 0..5. Vector128 t3 = (vec & Vector128.Create((byte)0xDF)) - Vector128.Create((byte)'A'); - - // And correct the range into 10..15. The non-hex letters bytes become greater than 0x0f. + // And correct the range into 10..15. + // The non-hex letters bytes become greater than 0x0f. Vector128 t4 = AddSaturate(t3, Vector128.Create((byte)10)); - - // Convert '0'..'9' into nibbles 0..9. Non-digit bytes become greater than 0x0f. - // Finally choose the result: either valid nibble (0..9/10..15) or some byte greater than 0x0f. + // Convert '0'..'9' into nibbles 0..9. Non-digit bytes become + // greater than 0x0f. Finally choose the result: either valid nibble (0..9/10..15) + // or some byte greater than 0x0f. Vector128 nibbles = Vector128.Min(t2 - Vector128.Create((byte)0xF0), t4); - // Any high bit is a sign that input is not a valid hex data if (AddSaturate(nibbles, Vector128.Create((byte)(127 - 15))).ExtractMostSignificantBits() != 0) { // Invalid input break; } - Vector128 output; if (Ssse3.IsSupported) { - output = Ssse3.MultiplyAddAdjacent(nibbles, Vector128.Create((short)0x0110).AsSByte()).AsByte(); + output = Ssse3.MultiplyAddAdjacent(nibbles, + Vector128.Create((short)0x0110).AsSByte()).AsByte(); } else { // MultiplyAddAdjacent doesn't exist on ARM - Vector128 evens = - AdvSimd.ShiftLeftLogicalWideningLower(AdvSimd.Arm64.UnzipEven(nibbles, Vector128.Create((byte)1)).GetLower(), 6); - Vector128 odds = AdvSimd.Arm64.TransposeOdd(nibbles, Vector128.Zero).AsUInt16(); + Vector128 evens = AdvSimd.ShiftLeftLogicalWideningLower( + AdvSimd.Arm64.UnzipEven(nibbles.AsInt16(), + Vector128.Create((byte)1).AsInt16()).GetLower(), 12); + Vector128 odds = AdvSimd.Arm64.TransposeOdd( + nibbles.AsInt16(), + Vector128.Zero).AsInt32(); output = Vector128.Add(evens, odds).AsByte(); } - // Accumulate output in lower INT64 half and take care about endianness output = Vector128.Shuffle(output, Vector128.Create((byte)0, 2, 4, 6, 8, 10, 12, 14, 0, 0, 0, 0, 0, 0, 0, 0)); - // Store 8 bytes in dest by given offset Unsafe.WriteUnaligned(ref Unsafe.Add(ref destRef, offset / 2), output.AsUInt64().ToScalar()); @@ -322,15 +321,14 @@ public static bool TryDecodeFromUtf16_Vector128(ReadOnlySpan chars, Span lengthSubVector128) + if (offset > lengthSubTwoVector128) { - offset = lengthSubVector128; + offset = lengthSubTwoVector128; } } while (true); - // Non-ASCII data detected - fall back to the scalar routine. + // Fall back to the scalar routine in case of invalid input. return TryDecodeFromUtf16(chars.Slice((int)offset), bytes.Slice((int)offset / 2), out _); // Remove once these are exposed as cross-plat helpers From 5c3660a6407ac8be228690fb1ced6c865bad7e61 Mon Sep 17 00:00:00 2001 From: EgorBo Date: Thu, 23 Feb 2023 14:16:44 +0100 Subject: [PATCH 03/11] oops --- src/libraries/Common/src/System/HexConverter.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libraries/Common/src/System/HexConverter.cs b/src/libraries/Common/src/System/HexConverter.cs index 32b8809a558be8..309845ca4d9b20 100644 --- a/src/libraries/Common/src/System/HexConverter.cs +++ b/src/libraries/Common/src/System/HexConverter.cs @@ -1,5 +1,3 @@ -#define SYSTEM_PRIVATE_CORELIB - // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. From e37e7f85441be3608d6a8a0f1e12173dbb49ddcf Mon Sep 17 00:00:00 2001 From: EgorBo Date: Thu, 23 Feb 2023 14:43:13 +0100 Subject: [PATCH 04/11] Improve codegen --- src/libraries/Common/src/System/HexConverter.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/libraries/Common/src/System/HexConverter.cs b/src/libraries/Common/src/System/HexConverter.cs index 309845ca4d9b20..ee92be733063b9 100644 --- a/src/libraries/Common/src/System/HexConverter.cs +++ b/src/libraries/Common/src/System/HexConverter.cs @@ -262,11 +262,6 @@ public static bool TryDecodeFromUtf16_Vector128(ReadOnlySpan chars, Span vec1 = Vector128.LoadUnsafe(ref srcRef, offset); Vector128 vec2 = Vector128.LoadUnsafe(ref srcRef, offset + (nuint)Vector128.Count); - if (!Utf16Utility.AllCharsInVector128AreAscii(vec1 | vec2)) - { - // Invalid input - break; - } Vector128 vec = Vector128.Narrow(vec1, vec2); // Based on "Algorithm #3" https://github.com/WojciechMula/toys/blob/master/simd-parse-hex/geoff_algorithm.cpp @@ -287,9 +282,10 @@ public static bool TryDecodeFromUtf16_Vector128(ReadOnlySpan chars, Span nibbles = Vector128.Min(t2 - Vector128.Create((byte)0xF0), t4); // Any high bit is a sign that input is not a valid hex data - if (AddSaturate(nibbles, Vector128.Create((byte)(127 - 15))).ExtractMostSignificantBits() != 0) + if (!Utf16Utility.AllCharsInVector128AreAscii(vec1 | vec2) || + AddSaturate(nibbles, Vector128.Create((byte)(127 - 15))).ExtractMostSignificantBits() != 0) { - // Invalid input + // Input is either non-ASCII or invalid hex data break; } Vector128 output; From 24943031e3148dff00cd39072939f3f740785ada Mon Sep 17 00:00:00 2001 From: EgorBo Date: Thu, 23 Feb 2023 21:25:32 +0100 Subject: [PATCH 05/11] Fix arm64 --- src/libraries/Common/src/System/HexConverter.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/libraries/Common/src/System/HexConverter.cs b/src/libraries/Common/src/System/HexConverter.cs index ee92be733063b9..95afe5d2bdc588 100644 --- a/src/libraries/Common/src/System/HexConverter.cs +++ b/src/libraries/Common/src/System/HexConverter.cs @@ -296,14 +296,15 @@ public static bool TryDecodeFromUtf16_Vector128(ReadOnlySpan chars, Span evens = AdvSimd.ShiftLeftLogicalWideningLower( - AdvSimd.Arm64.UnzipEven(nibbles.AsInt16(), - Vector128.Create((byte)1).AsInt16()).GetLower(), 12); - Vector128 odds = AdvSimd.Arm64.TransposeOdd( - nibbles.AsInt16(), - Vector128.Zero).AsInt32(); - output = Vector128.Add(evens, odds).AsByte(); + // Workaround for missing MultiplyAddAdjacent on ARM + Vector128 ones = Vector128.Create(0x10010).AsInt16(); + Vector128 vl = AdvSimd.Multiply( + AdvSimd.ZeroExtendWideningLower(nibbles.GetLower()).AsInt16(), ones).AsInt16(); + Vector128 vu = AdvSimd.Multiply( + AdvSimd.ZeroExtendWideningLower(nibbles.GetUpper()).AsInt16(), ones).AsInt16(); + output = AdvSimd.AddSaturate( + AdvSimd.Arm64.UnzipEven(vl, vu), + AdvSimd.Arm64.UnzipOdd(vl, vu)).AsByte(); } // Accumulate output in lower INT64 half and take care about endianness output = Vector128.Shuffle(output, Vector128.Create((byte)0, 2, 4, 6, 8, 10, 12, 14, 0, 0, 0, 0, 0, 0, 0, 0)); From 23b28d1adf9cd7c27f165f2b6612f6632a5faad8 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Wed, 29 Mar 2023 17:37:35 +0200 Subject: [PATCH 06/11] Update src/libraries/Common/src/System/HexConverter.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günther Foidl --- src/libraries/Common/src/System/HexConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Common/src/System/HexConverter.cs b/src/libraries/Common/src/System/HexConverter.cs index 95afe5d2bdc588..e6e468ba05678d 100644 --- a/src/libraries/Common/src/System/HexConverter.cs +++ b/src/libraries/Common/src/System/HexConverter.cs @@ -324,7 +324,7 @@ public static bool TryDecodeFromUtf16_Vector128(ReadOnlySpan chars, Span AddSaturate(Vector128 a, Vector128 b) => From 0fa90304857e8616123bf0bbf34aaca2a24d1b14 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Wed, 29 Mar 2023 17:57:40 +0200 Subject: [PATCH 07/11] Update src/libraries/Common/src/System/HexConverter.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günther Foidl --- src/libraries/Common/src/System/HexConverter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libraries/Common/src/System/HexConverter.cs b/src/libraries/Common/src/System/HexConverter.cs index e6e468ba05678d..d838ea01ba7ad9 100644 --- a/src/libraries/Common/src/System/HexConverter.cs +++ b/src/libraries/Common/src/System/HexConverter.cs @@ -321,7 +321,8 @@ public static bool TryDecodeFromUtf16_Vector128(ReadOnlySpan chars, Span Date: Wed, 29 Mar 2023 18:00:49 +0200 Subject: [PATCH 08/11] Address feedback --- .../Common/src/System/HexConverter.cs | 14 ++------- .../System/Runtime/Intrinsics/Vector128.cs | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/libraries/Common/src/System/HexConverter.cs b/src/libraries/Common/src/System/HexConverter.cs index 050459e918ef69..66badb55ff94f6 100644 --- a/src/libraries/Common/src/System/HexConverter.cs +++ b/src/libraries/Common/src/System/HexConverter.cs @@ -225,8 +225,6 @@ public static char ToCharLower(int value) public static bool TryDecodeFromUtf16(ReadOnlySpan chars, Span bytes) { - Debug.Assert(chars.Length <= checked(bytes.Length * 2)); - #if SYSTEM_PRIVATE_CORELIB if (BitConverter.IsLittleEndian && (Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && chars.Length >= Vector128.Count * 2) @@ -265,20 +263,20 @@ public static bool TryDecodeFromUtf16_Vector128(ReadOnlySpan chars, Span t1 = vec + Vector128.Create((byte)(0xFF - '9')); // And then correct the range to 0xf0..0xf9. // All other bytes become less than 0xf0. - Vector128 t2 = SubtractSaturate(t1, Vector128.Create((byte)6)); + Vector128 t2 = Vector128.SubtractSaturate(t1, Vector128.Create((byte)6)); // Convert into uppercase 'a'..'f' => 'A'..'F' and // move hex letter 'A'..'F' into range 0..5. Vector128 t3 = (vec & Vector128.Create((byte)0xDF)) - Vector128.Create((byte)'A'); // And correct the range into 10..15. // The non-hex letters bytes become greater than 0x0f. - Vector128 t4 = AddSaturate(t3, Vector128.Create((byte)10)); + Vector128 t4 = Vector128.AddSaturate(t3, Vector128.Create((byte)10)); // Convert '0'..'9' into nibbles 0..9. Non-digit bytes become // greater than 0x0f. Finally choose the result: either valid nibble (0..9/10..15) // or some byte greater than 0x0f. Vector128 nibbles = Vector128.Min(t2 - Vector128.Create((byte)0xF0), t4); // Any high bit is a sign that input is not a valid hex data if (!Utf16Utility.AllCharsInVector128AreAscii(vec1 | vec2) || - AddSaturate(nibbles, Vector128.Create((byte)(127 - 15))).ExtractMostSignificantBits() != 0) + Vector128.AddSaturate(nibbles, Vector128.Create((byte)(127 - 15))).ExtractMostSignificantBits() != 0) { // Input is either non-ASCII or invalid hex data break; @@ -320,12 +318,6 @@ public static bool TryDecodeFromUtf16_Vector128(ReadOnlySpan chars, Span AddSaturate(Vector128 a, Vector128 b) => - Sse2.IsSupported ? Sse2.AddSaturate(a, b) : AdvSimd.AddSaturate(a, b); - static Vector128 SubtractSaturate(Vector128 a, Vector128 b) => - Sse2.IsSupported ? Sse2.SubtractSaturate(a, b) : AdvSimd.SubtractSaturate(a, b); } #endif diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector128.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector128.cs index a8853d950e1cd6..557c61b70e8a59 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector128.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector128.cs @@ -3256,5 +3256,35 @@ internal static Vector128 UnpackHigh(Vector128 left, Vector128 } return AdvSimd.Arm64.ZipHigh(left, right); } + + // TODO: Make generic versions of these public, see https://github.com/dotnet/runtime/issues/82559 + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector128 AddSaturate(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.AddSaturate(left, right); + } + else if (!AdvSimd.Arm64.IsSupported) + { + ThrowHelper.ThrowNotSupportedException(); + } + return AdvSimd.AddSaturate(left, right); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector128 SubtractSaturate(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.SubtractSaturate(left, right); + } + else if (!AdvSimd.Arm64.IsSupported) + { + ThrowHelper.ThrowNotSupportedException(); + } + return AdvSimd.SubtractSaturate(left, right); + } } } From c800e10381119a1bb6428a914f75cc671ed7e127 Mon Sep 17 00:00:00 2001 From: EgorBo Date: Wed, 29 Mar 2023 19:13:21 +0200 Subject: [PATCH 09/11] Add mixed case test cases --- src/libraries/Common/src/System/HexConverter.cs | 2 +- .../tests/System/Convert.FromHexString.cs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libraries/Common/src/System/HexConverter.cs b/src/libraries/Common/src/System/HexConverter.cs index 9c099afdca5cbb..9c01fda8786993 100644 --- a/src/libraries/Common/src/System/HexConverter.cs +++ b/src/libraries/Common/src/System/HexConverter.cs @@ -314,7 +314,7 @@ public static bool TryDecodeFromUtf16_Vector128(ReadOnlySpan chars, Span(() => Convert.FromHexString(hex + " ")); Assert.Throws(() => Convert.FromHexString("\uAAAA" + hex)); } From dd913d826aa6e14398a410db14ab01e345e7dbbe Mon Sep 17 00:00:00 2001 From: EgorBo Date: Fri, 31 Mar 2023 01:05:04 +0200 Subject: [PATCH 10/11] fix test --- .../tests/System/Convert.FromHexString.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Runtime.Extensions/tests/System/Convert.FromHexString.cs b/src/libraries/System.Runtime.Extensions/tests/System/Convert.FromHexString.cs index 672c19cc2d581d..6ed2c2e778007b 100644 --- a/src/libraries/System.Runtime.Extensions/tests/System/Convert.FromHexString.cs +++ b/src/libraries/System.Runtime.Extensions/tests/System/Convert.FromHexString.cs @@ -116,8 +116,8 @@ public static void ToHexFromHexRoundtrip() hex.Substring(hex.Length / 2).ToLowerInvariant(); string mixedCase2 = hex.Substring(0, hex.Length / 2).ToLowerInvariant() + hex.Substring(hex.Length / 2).ToUpperInvariant(); - AssertEqual(data, Convert.FromHexString(mixedCase1)); - AssertEqual(data, Convert.FromHexString(mixedCase2)); + Assert.Equal(data, Convert.FromHexString(mixedCase1)); + Assert.Equal(data, Convert.FromHexString(mixedCase2)); Assert.Throws(() => Convert.FromHexString(hex + " ")); Assert.Throws(() => Convert.FromHexString("\uAAAA" + hex)); } From 5c22b52c651272431e98e7049783d350612b9f50 Mon Sep 17 00:00:00 2001 From: EgorBo Date: Fri, 31 Mar 2023 02:23:56 +0200 Subject: [PATCH 11/11] Apply Tanner's suggestion --- src/libraries/Common/src/System/HexConverter.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/libraries/Common/src/System/HexConverter.cs b/src/libraries/Common/src/System/HexConverter.cs index 9c01fda8786993..b80e404442b02c 100644 --- a/src/libraries/Common/src/System/HexConverter.cs +++ b/src/libraries/Common/src/System/HexConverter.cs @@ -290,14 +290,10 @@ public static bool TryDecodeFromUtf16_Vector128(ReadOnlySpan chars, Span ones = Vector128.Create(0x10010).AsInt16(); - Vector128 vl = AdvSimd.Multiply( - AdvSimd.ZeroExtendWideningLower(nibbles.GetLower()).AsInt16(), ones).AsInt16(); - Vector128 vu = AdvSimd.Multiply( - AdvSimd.ZeroExtendWideningLower(nibbles.GetUpper()).AsInt16(), ones).AsInt16(); - output = AdvSimd.AddSaturate( - AdvSimd.Arm64.UnzipEven(vl, vu), - AdvSimd.Arm64.UnzipOdd(vl, vu)).AsByte(); + Vector128 even = AdvSimd.Arm64.TransposeEven(nibbles, Vector128.Zero).AsInt16(); + Vector128 odd = AdvSimd.Arm64.TransposeOdd(nibbles, Vector128.Zero).AsInt16(); + even = AdvSimd.ShiftLeftLogical(even, 4).AsInt16(); + output = AdvSimd.AddSaturate(even, odd).AsByte(); } // Accumulate output in lower INT64 half and take care about endianness output = Vector128.Shuffle(output, Vector128.Create((byte)0, 2, 4, 6, 8, 10, 12, 14, 0, 0, 0, 0, 0, 0, 0, 0));