| | | 1 | | // Licensed to the .NET Foundation under one or more agreements. |
| | | 2 | | // The .NET Foundation licenses this file to you under the MIT license. |
| | | 3 | | |
| | | 4 | | using System.Diagnostics; |
| | | 5 | | using System.Runtime.CompilerServices; |
| | | 6 | | using System.Numerics; |
| | | 7 | | |
| | | 8 | | #if SYSTEM_PRIVATE_CORELIB |
| | | 9 | | using System.Runtime.InteropServices; |
| | | 10 | | using System.Runtime.Intrinsics; |
| | | 11 | | using System.Runtime.Intrinsics.Arm; |
| | | 12 | | using System.Runtime.Intrinsics.Wasm; |
| | | 13 | | using System.Runtime.Intrinsics.X86; |
| | | 14 | | using System.Text; |
| | | 15 | | using System.Text.Unicode; |
| | | 16 | | #endif |
| | | 17 | | |
| | | 18 | | namespace System |
| | | 19 | | { |
| | | 20 | | internal static class HexConverter |
| | | 21 | | { |
| | | 22 | | public enum Casing : uint |
| | | 23 | | { |
| | | 24 | | // Output [ '0' .. '9' ] and [ 'A' .. 'F' ]. |
| | | 25 | | Upper = 0, |
| | | 26 | | |
| | | 27 | | // Output [ '0' .. '9' ] and [ 'a' .. 'f' ]. |
| | | 28 | | // This works because values in the range [ 0x30 .. 0x39 ] ([ '0' .. '9' ]) |
| | | 29 | | // already have the 0x20 bit set, so ORing them with 0x20 is a no-op, |
| | | 30 | | // while outputs in the range [ 0x41 .. 0x46 ] ([ 'A' .. 'F' ]) |
| | | 31 | | // don't have the 0x20 bit set, so ORing them maps to |
| | | 32 | | // [ 0x61 .. 0x66 ] ([ 'a' .. 'f' ]), which is what we want. |
| | | 33 | | Lower = 0x2020U, |
| | | 34 | | } |
| | | 35 | | |
| | | 36 | | // We want to pack the incoming byte into a single integer [ 0000 HHHH 0000 LLLL ], |
| | | 37 | | // where HHHH and LLLL are the high and low nibbles of the incoming byte. Then |
| | | 38 | | // subtract this integer from a constant minuend as shown below. |
| | | 39 | | // |
| | | 40 | | // [ 1000 1001 1000 1001 ] |
| | | 41 | | // - [ 0000 HHHH 0000 LLLL ] |
| | | 42 | | // ========================= |
| | | 43 | | // [ *YYY **** *ZZZ **** ] |
| | | 44 | | // |
| | | 45 | | // The end result of this is that YYY is 0b000 if HHHH <= 9, and YYY is 0b111 if HHHH >= 10. |
| | | 46 | | // Similarly, ZZZ is 0b000 if LLLL <= 9, and ZZZ is 0b111 if LLLL >= 10. |
| | | 47 | | // (We don't care about the value of asterisked bits.) |
| | | 48 | | // |
| | | 49 | | // To turn a nibble in the range [ 0 .. 9 ] into hex, we calculate hex := nibble + 48 (ascii '0'). |
| | | 50 | | // To turn a nibble in the range [ 10 .. 15 ] into hex, we calculate hex := nibble - 10 + 65 (ascii 'A'). |
| | | 51 | | // => hex := nibble + 55. |
| | | 52 | | // The difference in the starting ASCII offset is (55 - 48) = 7, depending on whether the nibble is <= 9 or >= 1 |
| | | 53 | | // Since 7 is 0b111, this conveniently matches the YYY or ZZZ value computed during the earlier subtraction. |
| | | 54 | | |
| | | 55 | | // The commented out code below is code that directly implements the logic described above. |
| | | 56 | | |
| | | 57 | | // uint packedOriginalValues = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU); |
| | | 58 | | // uint difference = 0x8989U - packedOriginalValues; |
| | | 59 | | // uint add7Mask = (difference & 0x7070U) >> 4; // line YYY and ZZZ back up with the packed values |
| | | 60 | | // uint packedResult = packedOriginalValues + add7Mask + 0x3030U /* ascii '0' */; |
| | | 61 | | |
| | | 62 | | // The code below is equivalent to the commented out code above but has been tweaked |
| | | 63 | | // to allow codegen to make some extra optimizations. |
| | | 64 | | |
| | | 65 | | // The low byte of the packed result contains the hex representation of the incoming byte's low nibble. |
| | | 66 | | // The adjacent byte of the packed result contains the hex representation of the incoming byte's high nibble. |
| | | 67 | | |
| | | 68 | | // Finally, write to the output buffer starting with the *highest* index so that codegen can |
| | | 69 | | // elide all but the first bounds check. (This only works if 'startingIndex' is a compile-time constant.) |
| | | 70 | | |
| | | 71 | | // The JIT can elide bounds checks if 'startingIndex' is constant and if the caller is |
| | | 72 | | // writing to a span of known length (or the caller has already checked the bounds of the |
| | | 73 | | // furthest access). |
| | | 74 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 75 | | public static void ToBytesBuffer(byte value, Span<byte> buffer, int startingIndex = 0, Casing casing = Casing.Up |
| | | 76 | | { |
| | | 77 | | uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U; |
| | | 78 | | uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing; |
| | | 79 | | |
| | | 80 | | buffer[startingIndex + 1] = (byte)packedResult; |
| | | 81 | | buffer[startingIndex] = (byte)(packedResult >> 8); |
| | | 82 | | } |
| | | 83 | | |
| | | 84 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 85 | | public static void ToCharsBuffer(byte value, Span<char> buffer, int startingIndex = 0, Casing casing = Casing.Up |
| | | 86 | | { |
| | | 87 | | uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U; |
| | | 88 | | uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing; |
| | | 89 | | |
| | | 90 | | buffer[startingIndex + 1] = (char)(packedResult & 0xFF); |
| | | 91 | | buffer[startingIndex] = (char)(packedResult >> 8); |
| | | 92 | | } |
| | | 93 | | |
| | | 94 | | #if SYSTEM_PRIVATE_CORELIB |
| | | 95 | | // Converts Vector128<byte> into 2xVector128<byte> ASCII Hex representation |
| | | 96 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 97 | | [CompExactlyDependsOn(typeof(Ssse3))] |
| | | 98 | | [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] |
| | | 99 | | internal static (Vector128<byte>, Vector128<byte>) AsciiToHexVector128(Vector128<byte> src, Vector128<byte> hexM |
| | | 100 | | { |
| | | 101 | | Debug.Assert(Ssse3.IsSupported || AdvSimd.Arm64.IsSupported); |
| | | 102 | | |
| | | 103 | | // The algorithm is simple: a single srcVec (contains the whole 16b Guid) is converted |
| | | 104 | | // into nibbles and then, via hexMap, converted into a HEX representation via |
| | | 105 | | // Shuffle(nibbles, srcVec). ASCII is then expanded to UTF-16. |
| | | 106 | | Vector128<byte> shiftedSrc = Vector128.ShiftRightLogical(src.AsUInt64(), 4).AsByte(); |
| | | 107 | | Vector128<byte> lowNibbles = Vector128.UnpackLow(shiftedSrc, src); |
| | | 108 | | Vector128<byte> highNibbles = Vector128.UnpackHigh(shiftedSrc, src); |
| | | 109 | | |
| | | 110 | | return ( |
| | | 111 | | Vector128.ShuffleNative(hexMap, lowNibbles & Vector128.Create((byte)0xF)), |
| | | 112 | | Vector128.ShuffleNative(hexMap, highNibbles & Vector128.Create((byte)0xF)) |
| | | 113 | | ); |
| | | 114 | | } |
| | | 115 | | |
| | | 116 | | [CompExactlyDependsOn(typeof(Ssse3))] |
| | | 117 | | [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] |
| | | 118 | | private static void EncodeTo_Vector128<TChar>(ReadOnlySpan<byte> source, Span<TChar> destination, Casing casing) |
| | | 119 | | { |
| | | 120 | | Debug.Assert(source.Length >= (Vector128<TChar>.Count / 2)); |
| | | 121 | | |
| | | 122 | | ref byte srcRef = ref MemoryMarshal.GetReference(source); |
| | | 123 | | ref TChar destRef = ref MemoryMarshal.GetReference(destination); |
| | | 124 | | |
| | | 125 | | Vector128<byte> hexMap = casing == Casing.Upper ? |
| | | 126 | | Vector128.Create((byte)'0', (byte)'1', (byte)'2', (byte)'3', |
| | | 127 | | (byte)'4', (byte)'5', (byte)'6', (byte)'7', |
| | | 128 | | (byte)'8', (byte)'9', (byte)'A', (byte)'B', |
| | | 129 | | (byte)'C', (byte)'D', (byte)'E', (byte)'F') : |
| | | 130 | | Vector128.Create((byte)'0', (byte)'1', (byte)'2', (byte)'3', |
| | | 131 | | (byte)'4', (byte)'5', (byte)'6', (byte)'7', |
| | | 132 | | (byte)'8', (byte)'9', (byte)'a', (byte)'b', |
| | | 133 | | (byte)'c', (byte)'d', (byte)'e', (byte)'f'); |
| | | 134 | | |
| | | 135 | | nuint pos = 0; |
| | | 136 | | nuint lengthSubVector128 = (nuint)source.Length - (nuint)(Vector128<TChar>.Count / 2); |
| | | 137 | | do |
| | | 138 | | { |
| | | 139 | | // This implementation processes 4 or 8 bytes of input at once, it can be easily modified |
| | | 140 | | // to support 16 bytes at once, but that didn't demonstrate noticeable wins |
| | | 141 | | // for Converter.ToHexString (around 8% faster for large inputs) so |
| | | 142 | | // it focuses on small inputs instead. |
| | | 143 | | |
| | | 144 | | Vector128<byte> vec; |
| | | 145 | | |
| | | 146 | | if (typeof(TChar) == typeof(byte)) |
| | | 147 | | { |
| | | 148 | | vec = Vector128.CreateScalar(Unsafe.ReadUnaligned<ulong>(ref Unsafe.Add(ref srcRef, pos))).AsByte(); |
| | | 149 | | } |
| | | 150 | | else |
| | | 151 | | { |
| | | 152 | | Debug.Assert(typeof(TChar) == typeof(ushort)); |
| | | 153 | | vec = Vector128.CreateScalar(Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref srcRef, pos))).AsByte(); |
| | | 154 | | } |
| | | 155 | | |
| | | 156 | | // JIT is expected to eliminate all unused calculations |
| | | 157 | | (Vector128<byte> hexLow, _) = AsciiToHexVector128(vec, hexMap); |
| | | 158 | | |
| | | 159 | | if (typeof(TChar) == typeof(byte)) |
| | | 160 | | { |
| | | 161 | | hexLow.As<byte, TChar>().StoreUnsafe(ref destRef, pos * 2); |
| | | 162 | | } |
| | | 163 | | else |
| | | 164 | | { |
| | | 165 | | Debug.Assert(typeof(TChar) == typeof(ushort)); |
| | | 166 | | Vector128.WidenLower(hexLow).As<ushort, TChar>().StoreUnsafe(ref destRef, pos * 2); |
| | | 167 | | } |
| | | 168 | | |
| | | 169 | | pos += (nuint)(Vector128<TChar>.Count / 2); |
| | | 170 | | if (pos == (nuint)source.Length) |
| | | 171 | | { |
| | | 172 | | return; |
| | | 173 | | } |
| | | 174 | | |
| | | 175 | | // Overlap with the current chunk for trailing elements |
| | | 176 | | if (pos > lengthSubVector128) |
| | | 177 | | { |
| | | 178 | | pos = lengthSubVector128; |
| | | 179 | | } |
| | | 180 | | |
| | | 181 | | } while (true); |
| | | 182 | | } |
| | | 183 | | #endif |
| | | 184 | | |
| | | 185 | | public static void EncodeToUtf8(ReadOnlySpan<byte> source, Span<byte> utf8Destination, Casing casing = Casing.Up |
| | | 186 | | { |
| | | 187 | | Debug.Assert(utf8Destination.Length >= (source.Length * 2)); |
| | | 188 | | |
| | | 189 | | #if SYSTEM_PRIVATE_CORELIB |
| | | 190 | | if ((AdvSimd.Arm64.IsSupported || Ssse3.IsSupported) && (source.Length >= (Vector128<byte>.Count / 2))) |
| | | 191 | | { |
| | | 192 | | EncodeTo_Vector128(source, utf8Destination, casing); |
| | | 193 | | return; |
| | | 194 | | } |
| | | 195 | | #endif |
| | | 196 | | for (int pos = 0; pos < source.Length; pos++) |
| | | 197 | | { |
| | | 198 | | ToBytesBuffer(source[pos], utf8Destination, pos * 2, casing); |
| | | 199 | | } |
| | | 200 | | } |
| | | 201 | | |
| | | 202 | | public static void EncodeToUtf16(ReadOnlySpan<byte> source, Span<char> destination, Casing casing = Casing.Upper |
| | | 203 | | { |
| | | 204 | | Debug.Assert(destination.Length >= (source.Length * 2)); |
| | | 205 | | |
| | | 206 | | #if SYSTEM_PRIVATE_CORELIB |
| | | 207 | | if ((AdvSimd.Arm64.IsSupported || Ssse3.IsSupported) && (source.Length >= (Vector128<ushort>.Count / 2))) |
| | | 208 | | { |
| | | 209 | | EncodeTo_Vector128(source, Unsafe.BitCast<Span<char>, Span<ushort>>(destination), casing); |
| | | 210 | | return; |
| | | 211 | | } |
| | | 212 | | #endif |
| | | 213 | | for (int pos = 0; pos < source.Length; pos++) |
| | | 214 | | { |
| | | 215 | | ToCharsBuffer(source[pos], destination, pos * 2, casing); |
| | | 216 | | } |
| | | 217 | | } |
| | | 218 | | |
| | | 219 | | public static string ToString(ReadOnlySpan<byte> bytes, Casing casing = Casing.Upper) |
| | | 220 | | { |
| | | 221 | | #if NET |
| | | 222 | | SpanCasingPair args = new() { Bytes = bytes, Casing = casing }; |
| | | 223 | | return string.Create(bytes.Length * 2, args, static (chars, args) => |
| | | 224 | | EncodeToUtf16(args.Bytes, chars, args.Casing)); |
| | | 225 | | #else |
| | | 226 | | Span<char> result = (bytes.Length > 16) ? |
| | | 227 | | new char[bytes.Length * 2].AsSpan() : |
| | | 228 | | stackalloc char[bytes.Length * 2]; |
| | | 229 | | |
| | | 230 | | int pos = 0; |
| | | 231 | | foreach (byte b in bytes) |
| | | 232 | | { |
| | | 233 | | ToCharsBuffer(b, result, pos, casing); |
| | | 234 | | pos += 2; |
| | | 235 | | } |
| | | 236 | | return result.ToString(); |
| | | 237 | | #endif |
| | | 238 | | } |
| | | 239 | | |
| | | 240 | | private ref struct SpanCasingPair |
| | | 241 | | { |
| | | 242 | | public ReadOnlySpan<byte> Bytes { get; set; } |
| | | 243 | | public Casing Casing { get; set; } |
| | | 244 | | } |
| | | 245 | | |
| | | 246 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 247 | | public static char ToCharUpper(int value) |
| | 0 | 248 | | { |
| | 0 | 249 | | value &= 0xF; |
| | 0 | 250 | | value += '0'; |
| | | 251 | | |
| | 0 | 252 | | if (value > '9') |
| | 0 | 253 | | { |
| | 0 | 254 | | value += ('A' - ('9' + 1)); |
| | 0 | 255 | | } |
| | | 256 | | |
| | 0 | 257 | | return (char)value; |
| | 0 | 258 | | } |
| | | 259 | | |
| | | 260 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 261 | | public static char ToCharLower(int value) |
| | | 262 | | { |
| | | 263 | | value &= 0xF; |
| | | 264 | | value += '0'; |
| | | 265 | | |
| | | 266 | | if (value > '9') |
| | | 267 | | { |
| | | 268 | | value += ('a' - ('9' + 1)); |
| | | 269 | | } |
| | | 270 | | |
| | | 271 | | return (char)value; |
| | | 272 | | } |
| | | 273 | | |
| | | 274 | | public static bool TryDecodeFromUtf8(ReadOnlySpan<byte> utf8Source, Span<byte> destination, out int bytesProcess |
| | | 275 | | { |
| | | 276 | | #if SYSTEM_PRIVATE_CORELIB |
| | | 277 | | if (BitConverter.IsLittleEndian && (Ssse3.IsSupported || AdvSimd.Arm64.IsSupported || PackedSimd.IsSupported |
| | | 278 | | (utf8Source.Length >= Vector128<byte>.Count)) |
| | | 279 | | { |
| | | 280 | | return TryDecodeFrom_Vector128(utf8Source, destination, out bytesProcessed); |
| | | 281 | | } |
| | | 282 | | #endif |
| | | 283 | | return TryDecodeFromUtf8_Scalar(utf8Source, destination, out bytesProcessed); |
| | | 284 | | } |
| | | 285 | | |
| | | 286 | | public static bool TryDecodeFromUtf16(ReadOnlySpan<char> source, Span<byte> destination, out int charsProcessed) |
| | | 287 | | { |
| | | 288 | | #if SYSTEM_PRIVATE_CORELIB |
| | | 289 | | if (BitConverter.IsLittleEndian && (Ssse3.IsSupported || AdvSimd.Arm64.IsSupported || PackedSimd.IsSupported |
| | | 290 | | (source.Length >= (Vector128<ushort>.Count * 2))) |
| | | 291 | | { |
| | | 292 | | return TryDecodeFrom_Vector128(Unsafe.BitCast<ReadOnlySpan<char>, ReadOnlySpan<ushort>>(source), destina |
| | | 293 | | } |
| | | 294 | | #endif |
| | | 295 | | return TryDecodeFromUtf16_Scalar(source, destination, out charsProcessed); |
| | | 296 | | } |
| | | 297 | | |
| | | 298 | | #if SYSTEM_PRIVATE_CORELIB |
| | | 299 | | [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] |
| | | 300 | | [CompExactlyDependsOn(typeof(Ssse3))] |
| | | 301 | | [CompExactlyDependsOn(typeof(PackedSimd))] |
| | | 302 | | public static bool TryDecodeFrom_Vector128<TChar>(ReadOnlySpan<TChar> source, Span<byte> destination, out int el |
| | | 303 | | { |
| | | 304 | | Debug.Assert(Ssse3.IsSupported || AdvSimd.Arm64.IsSupported || PackedSimd.IsSupported); |
| | | 305 | | Debug.Assert(source.Length <= (destination.Length * 2)); |
| | | 306 | | Debug.Assert((source.Length % 2) == 0); |
| | | 307 | | |
| | | 308 | | int elementsReadPerIteration; |
| | | 309 | | |
| | | 310 | | if (typeof(TChar) == typeof(byte)) |
| | | 311 | | { |
| | | 312 | | elementsReadPerIteration = Vector128<byte>.Count; |
| | | 313 | | } |
| | | 314 | | else |
| | | 315 | | { |
| | | 316 | | Debug.Assert(typeof(TChar) == typeof(ushort)); |
| | | 317 | | elementsReadPerIteration = Vector128<ushort>.Count * 2; |
| | | 318 | | } |
| | | 319 | | Debug.Assert(source.Length >= elementsReadPerIteration); |
| | | 320 | | |
| | | 321 | | nuint offset = 0; |
| | | 322 | | nuint lengthSubElementsReadPerIteration = (nuint)source.Length - (nuint)elementsReadPerIteration; |
| | | 323 | | |
| | | 324 | | ref TChar srcRef = ref MemoryMarshal.GetReference(source); |
| | | 325 | | ref byte destRef = ref MemoryMarshal.GetReference(destination); |
| | | 326 | | |
| | | 327 | | do |
| | | 328 | | { |
| | | 329 | | // The algorithm is UTF8 so we'll be loading two UTF-16 vectors to narrow them into a |
| | | 330 | | // single UTF8 ASCII vector - the implementation can be shared with UTF8 paths. |
| | | 331 | | Vector128<byte> vec; |
| | | 332 | | |
| | | 333 | | if (typeof(TChar) == typeof(byte)) |
| | | 334 | | { |
| | | 335 | | vec = Vector128.LoadUnsafe(ref srcRef, offset).AsByte(); |
| | | 336 | | |
| | | 337 | | if (!Utf8Utility.AllBytesInVector128AreAscii(vec)) |
| | | 338 | | { |
| | | 339 | | // Input is non-ASCII |
| | | 340 | | break; |
| | | 341 | | } |
| | | 342 | | } |
| | | 343 | | else |
| | | 344 | | { |
| | | 345 | | Debug.Assert(typeof(TChar) == typeof(ushort)); |
| | | 346 | | |
| | | 347 | | Vector128<ushort> vec1 = Vector128.LoadUnsafe(ref srcRef, offset).AsUInt16(); |
| | | 348 | | Vector128<ushort> vec2 = Vector128.LoadUnsafe(ref srcRef, offset + (nuint)Vector128<ushort>.Count).A |
| | | 349 | | |
| | | 350 | | vec = Ascii.ExtractAsciiVector(vec1, vec2); |
| | | 351 | | |
| | | 352 | | if (!Utf16Utility.AllCharsInVectorAreAscii(vec1 | vec2)) |
| | | 353 | | { |
| | | 354 | | // Input is non-ASCII |
| | | 355 | | break; |
| | | 356 | | } |
| | | 357 | | } |
| | | 358 | | |
| | | 359 | | // Based on "Algorithm #3" https://github.com/WojciechMula/toys/blob/master/simd-parse-hex/geoff_algorit |
| | | 360 | | // by Geoff Langdale and Wojciech Mula |
| | | 361 | | // Move digits '0'..'9' into range 0xf6..0xff. |
| | | 362 | | Vector128<byte> t1 = vec + Vector128.Create<byte>(0xFF - '9'); |
| | | 363 | | |
| | | 364 | | // And then correct the range to 0xf0..0xf9. |
| | | 365 | | // All other bytes become less than 0xf0. |
| | | 366 | | Vector128<byte> t2 = Vector128.SubtractSaturate(t1, Vector128.Create<byte>(6)); |
| | | 367 | | |
| | | 368 | | // Convert into uppercase 'a'..'f' => 'A'..'F' and |
| | | 369 | | // move hex letter 'A'..'F' into range 0..5. |
| | | 370 | | Vector128<byte> t3 = (vec & Vector128.Create<byte>(0xDF)) - Vector128.Create((byte)'A'); |
| | | 371 | | |
| | | 372 | | // And correct the range into 10..15. |
| | | 373 | | // The non-hex letters bytes become greater than 0x0f. |
| | | 374 | | Vector128<byte> t4 = Vector128.AddSaturate(t3, Vector128.Create<byte>(10)); |
| | | 375 | | |
| | | 376 | | // Convert '0'..'9' into nibbles 0..9. Non-digit bytes become |
| | | 377 | | // greater than 0x0f. Finally choose the result: either valid nibble (0..9/10..15) |
| | | 378 | | // or some byte greater than 0x0f. |
| | | 379 | | Vector128<byte> nibbles = Vector128.Min(t2 - Vector128.Create<byte>(0xF0), t4); |
| | | 380 | | |
| | | 381 | | // Any high bit is a sign that input is not a valid hex data |
| | | 382 | | if (Vector128.AddSaturate(nibbles, Vector128.Create<byte>(127 - 15)).ExtractMostSignificantBits() != 0) |
| | | 383 | | { |
| | | 384 | | // Input is invalid hex data |
| | | 385 | | break; |
| | | 386 | | } |
| | | 387 | | |
| | | 388 | | Vector128<byte> output; |
| | | 389 | | if (Ssse3.IsSupported) |
| | | 390 | | { |
| | | 391 | | output = Ssse3.MultiplyAddAdjacent(nibbles, Vector128.Create<short>(0x0110).AsSByte()).AsByte(); |
| | | 392 | | } |
| | | 393 | | else if (AdvSimd.Arm64.IsSupported) |
| | | 394 | | { |
| | | 395 | | // Workaround for missing MultiplyAddAdjacent on ARM |
| | | 396 | | Vector128<short> even = AdvSimd.Arm64.TransposeEven(nibbles, Vector128<byte>.Zero).AsInt16(); |
| | | 397 | | Vector128<short> odd = AdvSimd.Arm64.TransposeOdd(nibbles, Vector128<byte>.Zero).AsInt16(); |
| | | 398 | | |
| | | 399 | | even = (even << 4).AsInt16(); |
| | | 400 | | output = AdvSimd.AddSaturate(even, odd).AsByte(); |
| | | 401 | | } |
| | | 402 | | else if (PackedSimd.IsSupported) |
| | | 403 | | { |
| | | 404 | | Vector128<byte> shiftedNibbles = nibbles << 4; |
| | | 405 | | Vector128<byte> zipped = PackedSimd.BitwiseSelect(nibbles, shiftedNibbles, Vector128.Create<ushort>( |
| | | 406 | | output = PackedSimd.AddPairwiseWidening(zipped).AsByte(); |
| | | 407 | | } |
| | | 408 | | else |
| | | 409 | | { |
| | | 410 | | // We explicitly recheck each IsSupported query to ensure that the trimmer can see which paths are l |
| | | 411 | | ThrowHelper.ThrowUnreachableException(); |
| | | 412 | | output = default; |
| | | 413 | | } |
| | | 414 | | |
| | | 415 | | // Accumulate output in lower INT64 half and take care about endianness |
| | | 416 | | output = Vector128.Shuffle(output, Vector128.Create((byte)0, 2, 4, 6, 8, 10, 12, 14, 0, 0, 0, 0, 0, 0, 0 |
| | | 417 | | |
| | | 418 | | // Store 8 bytes in dest by given offset |
| | | 419 | | Unsafe.WriteUnaligned(ref Unsafe.Add(ref destRef, offset / 2), output.AsUInt64().ToScalar()); |
| | | 420 | | |
| | | 421 | | offset += (nuint)elementsReadPerIteration; |
| | | 422 | | if (offset == (nuint)source.Length) |
| | | 423 | | { |
| | | 424 | | elementsProcessed = source.Length; |
| | | 425 | | return true; |
| | | 426 | | } |
| | | 427 | | |
| | | 428 | | // Overlap with the current chunk for trailing elements |
| | | 429 | | if (offset > lengthSubElementsReadPerIteration) |
| | | 430 | | { |
| | | 431 | | offset = lengthSubElementsReadPerIteration; |
| | | 432 | | } |
| | | 433 | | } |
| | | 434 | | while (true); |
| | | 435 | | |
| | | 436 | | // Fall back to the scalar routine in case of invalid input. |
| | | 437 | | bool fallbackResult; |
| | | 438 | | |
| | | 439 | | if (typeof(TChar) == typeof(byte)) |
| | | 440 | | { |
| | | 441 | | fallbackResult = TryDecodeFromUtf8_Scalar(Unsafe.BitCast<ReadOnlySpan<TChar>, ReadOnlySpan<byte>>(source |
| | | 442 | | } |
| | | 443 | | else |
| | | 444 | | { |
| | | 445 | | Debug.Assert(typeof(TChar) == typeof(ushort)); |
| | | 446 | | fallbackResult = TryDecodeFromUtf16_Scalar(Unsafe.BitCast<ReadOnlySpan<TChar>, ReadOnlySpan<char>>(sourc |
| | | 447 | | } |
| | | 448 | | |
| | | 449 | | elementsProcessed = (int)offset + elementsProcessed; |
| | | 450 | | return fallbackResult; |
| | | 451 | | } |
| | | 452 | | #endif |
| | | 453 | | |
| | | 454 | | private static bool TryDecodeFromUtf8_Scalar(ReadOnlySpan<byte> utf8Source, Span<byte> destination, out int byte |
| | | 455 | | { |
| | | 456 | | Debug.Assert((utf8Source.Length % 2) == 0, "Un-even number of characters provided"); |
| | | 457 | | Debug.Assert((utf8Source.Length / 2) == destination.Length, "Target buffer not right-sized for provided char |
| | | 458 | | |
| | | 459 | | int i = 0; |
| | | 460 | | int j = 0; |
| | | 461 | | int byteLo = 0; |
| | | 462 | | int byteHi = 0; |
| | | 463 | | |
| | | 464 | | while (j < destination.Length) |
| | | 465 | | { |
| | | 466 | | byteLo = FromChar(utf8Source[i + 1]); |
| | | 467 | | byteHi = FromChar(utf8Source[i]); |
| | | 468 | | |
| | | 469 | | // byteHi hasn't been shifted to the high half yet, so the only way the bitwise or produces this pattern |
| | | 470 | | // is if either byteHi or byteLo was not a hex character. |
| | | 471 | | if ((byteLo | byteHi) == 0xFF) |
| | | 472 | | { |
| | | 473 | | break; |
| | | 474 | | } |
| | | 475 | | |
| | | 476 | | destination[j++] = (byte)((byteHi << 4) | byteLo); |
| | | 477 | | i += 2; |
| | | 478 | | } |
| | | 479 | | |
| | | 480 | | if (byteLo == 0xFF) |
| | | 481 | | { |
| | | 482 | | i++; |
| | | 483 | | } |
| | | 484 | | |
| | | 485 | | bytesProcessed = i; |
| | | 486 | | return (byteLo | byteHi) != 0xFF; |
| | | 487 | | } |
| | | 488 | | |
| | | 489 | | private static bool TryDecodeFromUtf16_Scalar(ReadOnlySpan<char> source, Span<byte> destination, out int charsPr |
| | | 490 | | { |
| | | 491 | | Debug.Assert((source.Length % 2) == 0, "Un-even number of characters provided"); |
| | | 492 | | Debug.Assert((source.Length / 2) == destination.Length, "Target buffer not right-sized for provided characte |
| | | 493 | | |
| | | 494 | | int i = 0; |
| | | 495 | | int j = 0; |
| | | 496 | | int byteLo = 0; |
| | | 497 | | int byteHi = 0; |
| | | 498 | | |
| | | 499 | | while (j < destination.Length) |
| | | 500 | | { |
| | | 501 | | byteLo = FromChar(source[i + 1]); |
| | | 502 | | byteHi = FromChar(source[i]); |
| | | 503 | | |
| | | 504 | | // byteHi hasn't been shifted to the high half yet, so the only way the bitwise or produces this pattern |
| | | 505 | | // is if either byteHi or byteLo was not a hex character. |
| | | 506 | | if ((byteLo | byteHi) == 0xFF) |
| | | 507 | | { |
| | | 508 | | break; |
| | | 509 | | } |
| | | 510 | | |
| | | 511 | | destination[j++] = (byte)((byteHi << 4) | byteLo); |
| | | 512 | | i += 2; |
| | | 513 | | } |
| | | 514 | | |
| | | 515 | | if (byteLo == 0xFF) |
| | | 516 | | { |
| | | 517 | | i++; |
| | | 518 | | } |
| | | 519 | | |
| | | 520 | | charsProcessed = i; |
| | | 521 | | return (byteLo | byteHi) != 0xFF; |
| | | 522 | | } |
| | | 523 | | |
| | | 524 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 525 | | public static int FromChar(int c) |
| | | 526 | | { |
| | | 527 | | return (c >= CharToHexLookup.Length) ? 0xFF : CharToHexLookup[c]; |
| | | 528 | | } |
| | | 529 | | |
| | | 530 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 531 | | public static int FromUpperChar(int c) |
| | 4354 | 532 | | { |
| | 4354 | 533 | | return (c > 71) ? 0xFF : CharToHexLookup[c]; |
| | 4354 | 534 | | } |
| | | 535 | | |
| | | 536 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 537 | | public static int FromLowerChar(int c) |
| | | 538 | | { |
| | | 539 | | if ((uint)(c - '0') <= ('9' - '0')) |
| | | 540 | | { |
| | | 541 | | return c - '0'; |
| | | 542 | | } |
| | | 543 | | |
| | | 544 | | if ((uint)(c - 'a') <= ('f' - 'a')) |
| | | 545 | | { |
| | | 546 | | return c - 'a' + 10; |
| | | 547 | | } |
| | | 548 | | |
| | | 549 | | return 0xFF; |
| | | 550 | | } |
| | | 551 | | |
| | | 552 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 553 | | public static bool IsHexChar(int c) |
| | | 554 | | { |
| | | 555 | | if (IntPtr.Size == 8) |
| | | 556 | | { |
| | | 557 | | // This code path, when used, has no branches and doesn't depend on cache hits, |
| | | 558 | | // so it's faster and does not vary in speed depending on input data distribution. |
| | | 559 | | // We only use this logic on 64-bit systems, as using 64 bit values would otherwise |
| | | 560 | | // be much slower than just using the lookup table anyway (no hardware support). |
| | | 561 | | // The magic constant 18428868213665201664 is a 64 bit value containing 1s at the |
| | | 562 | | // indices corresponding to all the valid hex characters (ie. "0123456789ABCDEFabcdef") |
| | | 563 | | // minus 48 (ie. '0'), and backwards (so from the most significant bit and downwards). |
| | | 564 | | // The offset of 48 for each bit is necessary so that the entire range fits in 64 bits. |
| | | 565 | | // First, we subtract '0' to the input digit (after casting to uint to account for any |
| | | 566 | | // negative inputs). Note that even if this subtraction underflows, this happens before |
| | | 567 | | // the result is zero-extended to ulong, meaning that `i` will always have upper 32 bits |
| | | 568 | | // equal to 0. We then left shift the constant with this offset, and apply a bitmask that |
| | | 569 | | // has the highest bit set (the sign bit) if and only if `c` is in the ['0', '0' + 64) range. |
| | | 570 | | // Then we only need to check whether this final result is less than 0: this will only be |
| | | 571 | | // the case if both `i` was in fact the index of a set bit in the magic constant, and also |
| | | 572 | | // `c` was in the allowed range (this ensures that false positive bit shifts are ignored). |
| | | 573 | | ulong i = (uint)c - '0'; |
| | | 574 | | ulong shift = 18428868213665201664UL << (int)i; |
| | | 575 | | ulong mask = i - 64; |
| | | 576 | | |
| | | 577 | | return (long)(shift & mask) < 0 ? true : false; |
| | | 578 | | } |
| | | 579 | | |
| | | 580 | | return FromChar(c) != 0xFF; |
| | | 581 | | } |
| | | 582 | | |
| | | 583 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 584 | | public static bool IsHexUpperChar(int c) |
| | | 585 | | { |
| | | 586 | | return ((uint)(c - '0') <= 9) || ((uint)(c - 'A') <= ('F' - 'A')); |
| | | 587 | | } |
| | | 588 | | |
| | | 589 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 590 | | public static bool IsHexLowerChar(int c) |
| | | 591 | | { |
| | | 592 | | return ((uint)(c - '0') <= 9) || ((uint)(c - 'a') <= ('f' - 'a')); |
| | | 593 | | } |
| | | 594 | | |
| | | 595 | | /// <summary>Map from an ASCII char to its hex value, e.g. arr['b'] == 11. 0xFF means it's not a hex digit.</sum |
| | | 596 | | public static ReadOnlySpan<byte> CharToHexLookup => |
| | 4348 | 597 | | [ |
| | 4348 | 598 | | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15 |
| | 4348 | 599 | | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31 |
| | 4348 | 600 | | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47 |
| | 4348 | 601 | | 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63 |
| | 4348 | 602 | | 0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79 |
| | 4348 | 603 | | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95 |
| | 4348 | 604 | | 0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111 |
| | 4348 | 605 | | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127 |
| | 4348 | 606 | | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143 |
| | 4348 | 607 | | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159 |
| | 4348 | 608 | | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175 |
| | 4348 | 609 | | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191 |
| | 4348 | 610 | | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207 |
| | 4348 | 611 | | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223 |
| | 4348 | 612 | | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239 |
| | 4348 | 613 | | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 255 |
| | 4348 | 614 | | ]; |
| | | 615 | | } |
| | | 616 | | } |