Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

Add optimized UTF-8 validation and transcoding apis, hook them up to UTF8Encoding #21948

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
abd7add
Add optimized UTF-8 validation and transcoding logic
GrabYourPitchforks Mar 26, 2019
21721c2
Merge remote-tracking branch 'origin/master' into utf8_validation_apis
GrabYourPitchforks Mar 27, 2019
7198253
Merge commit 'aab338efb65808acc1ff9b7f95bd3dd5f0a6a3be' into utf8_val…
GrabYourPitchforks Apr 3, 2019
effeb6e
Hook up new UTF-8 logic through UTF8Encoding
GrabYourPitchforks Apr 3, 2019
4caf96a
Improve perf of "is ASCII?" inner loop in UTF-8 validation.
GrabYourPitchforks Apr 3, 2019
3f939ee
Remove SSE41.X64 optimization from AsciiUtility
GrabYourPitchforks Apr 3, 2019
2be465e
Merge remote-tracking branch 'origin/master' into utf8_validation_apis_3
GrabYourPitchforks Apr 4, 2019
fbb7246
Merge remote-tracking branch 'origin/master' into utf8_validation_apis_3
GrabYourPitchforks Apr 5, 2019
15a361a
Merge remote-tracking branch 'origin/master' into utf8_validation_apis_3
GrabYourPitchforks Apr 8, 2019
5995f81
Clarify that vector read is unaligned
GrabYourPitchforks Apr 8, 2019
c4e94df
Simplify vectorized logic; remove unnecessary adjustment
GrabYourPitchforks Apr 8, 2019
5db40d7
Merge remote-tracking branch 'origin/master' into utf8_validation_apis_3
GrabYourPitchforks Apr 9, 2019
f4519f5
PR feedback: GetElement(0) -> Sse2.StoreLow
GrabYourPitchforks Apr 9, 2019
9baf5be
PR feedback
GrabYourPitchforks Apr 9, 2019
2a3fe36
PR feedback: Enable SSE2 in Utf16Utility code
GrabYourPitchforks Apr 10, 2019
2d47f1b
Expand masks in Utf8Utility, fix const in fallback path
GrabYourPitchforks Apr 10, 2019
356ae17
Temporarily disable failing CoreFX tests
GrabYourPitchforks Apr 10, 2019
5257614
Fix incorrect Debug.Assert statements
GrabYourPitchforks Apr 10, 2019
6cc74a9
Add comments tracking JIT workarounds.
GrabYourPitchforks Apr 10, 2019
60b8c4f
Rename DWORD -> UInt32 throughout API surface
GrabYourPitchforks Apr 10, 2019
bae40fa
Re-flow Utf8Utility.Helpers
GrabYourPitchforks Apr 10, 2019
eda0c04
Merge remote-tracking branch 'origin/master' into utf8_validation_apis_3
GrabYourPitchforks Apr 11, 2019
3e09d17
PR feedback: Fix typos
GrabYourPitchforks Apr 11, 2019
56fa69d
PR feedback: CountNumberOfLeadingAsciiBytesFrom24BitInteger
GrabYourPitchforks Apr 11, 2019
8f2860d
PR feedback: Remove redundant endianess checks
GrabYourPitchforks Apr 11, 2019
ce13100
PR feedback: Validate nint definitions
GrabYourPitchforks Apr 11, 2019
c3aa431
PR feedback: Clarify charIsNonAscii vector usage
GrabYourPitchforks Apr 11, 2019
d8e2589
PR feedback: document tempUtf8CodeUnitCountAdjustment usage
GrabYourPitchforks Apr 11, 2019
7558272
Fix compilation failure in Utf16Utility
GrabYourPitchforks Apr 11, 2019
affc24a
PR feedback: Clarify 3-byte sequence processing
GrabYourPitchforks Apr 11, 2019
0adca8b
Add missing check to 3-byte processing logic
GrabYourPitchforks Apr 11, 2019
e307d14
Clarify comment in 3-byte processing
GrabYourPitchforks Apr 11, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,7 @@
<Compile Include="$(MSBuildThisFileDirectory)System\SystemException.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\ASCIIEncoding.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\ASCIIUtility.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\ASCIIUtility.Helpers.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\StringBuilderCache.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\CodePageDataItem.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\Decoder.cs" />
Expand Down Expand Up @@ -799,13 +800,17 @@
<Compile Include="$(MSBuildThisFileDirectory)System\Text\UnicodeDebug.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\UnicodeEncoding.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\UnicodeUtility.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\Utf16Utility.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\UTF32Encoding.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\UTF7Encoding.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\UTF8Encoding.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\ValueStringBuilder.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\Unicode\Utf16Utility.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\Unicode\Utf16Utility.Validation.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\Unicode\Utf8.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\Unicode\Utf8Utility.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\Unicode\Utf8Utility.Helpers.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\Unicode\Utf8Utility.Transcoding.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Text\Unicode\Utf8Utility.Validation.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\TimeSpan.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\ThreadAttributes.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\AbandonedMutexException.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Text;
using System.Text.Unicode;
using Internal.Runtime.CompilerServices;

namespace System.Globalization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Text;
using System.Text.Unicode;
using Internal.Runtime.CompilerServices;

#if BIT64
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using System.Buffers;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Unicode;
using Internal.Runtime.CompilerServices;

#if BIT64
Expand Down
12 changes: 6 additions & 6 deletions src/System.Private.CoreLib/shared/System/Text/ASCIIEncoding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ private unsafe int GetByteCountCommon(char* pChars, int charCount)
// Common helper method for all non-EncoderNLS entry points to GetByteCount.
// A modification of this method should be copied in to each of the supported encodings: ASCII, UTF8, UTF16, UTF32.

Debug.Assert(charCount >= 0, "Caller should't specify negative length buffer.");
Debug.Assert(charCount >= 0, "Caller shouldn't specify negative length buffer.");
Debug.Assert(pChars != null || charCount == 0, "Input pointer shouldn't be null if non-zero length specified.");

// First call into the fast path.
Expand Down Expand Up @@ -340,9 +340,9 @@ private unsafe int GetBytesCommon(char* pChars, int charCount, byte* pBytes, int
// Common helper method for all non-EncoderNLS entry points to GetBytes.
// A modification of this method should be copied in to each of the supported encodings: ASCII, UTF8, UTF16, UTF32.

Debug.Assert(charCount >= 0, "Caller should't specify negative length buffer.");
Debug.Assert(charCount >= 0, "Caller shouldn't specify negative length buffer.");
Debug.Assert(pChars != null || charCount == 0, "Input pointer shouldn't be null if non-zero length specified.");
Debug.Assert(byteCount >= 0, "Caller should't specify negative length buffer.");
Debug.Assert(byteCount >= 0, "Caller shouldn't specify negative length buffer.");
Debug.Assert(pBytes != null || byteCount == 0, "Input pointer shouldn't be null if non-zero length specified.");

// First call into the fast path.
Expand Down Expand Up @@ -496,7 +496,7 @@ private unsafe int GetCharCountCommon(byte* pBytes, int byteCount)
// Common helper method for all non-DecoderNLS entry points to GetCharCount.
// A modification of this method should be copied in to each of the supported encodings: ASCII, UTF8, UTF16, UTF32.

Debug.Assert(byteCount >= 0, "Caller should't specify negative length buffer.");
Debug.Assert(byteCount >= 0, "Caller shouldn't specify negative length buffer.");
Debug.Assert(pBytes != null || byteCount == 0, "Input pointer shouldn't be null if non-zero length specified.");

// First call into the fast path.
Expand Down Expand Up @@ -624,9 +624,9 @@ private unsafe int GetCharsCommon(byte* pBytes, int byteCount, char* pChars, int
// Common helper method for all non-DecoderNLS entry points to GetChars.
// A modification of this method should be copied in to each of the supported encodings: ASCII, UTF8, UTF16, UTF32.

Debug.Assert(byteCount >= 0, "Caller should't specify negative length buffer.");
Debug.Assert(byteCount >= 0, "Caller shouldn't specify negative length buffer.");
Debug.Assert(pBytes != null || byteCount == 0, "Input pointer shouldn't be null if non-zero length specified.");
Debug.Assert(charCount >= 0, "Caller should't specify negative length buffer.");
Debug.Assert(charCount >= 0, "Caller shouldn't specify negative length buffer.");
Debug.Assert(pChars != null || charCount == 0, "Input pointer shouldn't be null if non-zero length specified.");

// First call into the fast path.
Expand Down
108 changes: 108 additions & 0 deletions src/System.Private.CoreLib/shared/System/Text/ASCIIUtility.Helpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics.X86;

namespace System.Text
{
internal static partial class ASCIIUtility
{
/// <summary>
/// A mask which selects only the high bit of each byte of the given <see cref="uint"/>.
/// </summary>
private const uint UInt32HighBitsOnlyMask = 0x80808080u;

/// <summary>
/// A mask which selects only the high bit of each byte of the given <see cref="ulong"/>.
/// </summary>
private const ulong UInt64HighBitsOnlyMask = 0x80808080_80808080ul;

/// <summary>
/// Returns <see langword="true"/> iff all bytes in <paramref name="value"/> are ASCII.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool AllBytesInUInt32AreAscii(uint value)
{
// If the high bit of any byte is set, that byte is non-ASCII.

return (value & UInt32HighBitsOnlyMask) == 0;
}

/// <summary>
/// Given a DWORD which represents a four-byte buffer read in machine endianness, and which
/// the caller has asserted contains a non-ASCII byte *somewhere* in the data, counts the
/// number of consecutive ASCII bytes starting from the beginning of the buffer. Returns
/// a value 0 - 3, inclusive. (The caller is responsible for ensuring that the buffer doesn't
/// contain all-ASCII data.)
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static uint CountNumberOfLeadingAsciiBytesFromUInt32WithSomeNonAsciiData(uint value)
{
Debug.Assert(!AllBytesInUInt32AreAscii(value), "Caller shouldn't provide an all-ASCII value.");
GrabYourPitchforks marked this conversation as resolved.
Show resolved Hide resolved

// Use BMI1 directly rather than going through BitOperations. We only see a perf gain here
// if we're able to emit a real tzcnt instruction; the software fallback used by BitOperations
// is too slow for our purposes since we can provide our own faster, specialized software fallback.

if (Bmi1.IsSupported)
{
Debug.Assert(BitConverter.IsLittleEndian);
return Bmi1.TrailingZeroCount(value & UInt32HighBitsOnlyMask) >> 3;
}

// Couldn't emit tzcnt, use specialized software fallback.
// The 'allBytesUpToNowAreAscii' DWORD uses bit twiddling to hold a 1 or a 0 depending
// on whether all processed bytes were ASCII. Then we accumulate all of the
// results to calculate how many consecutive ASCII bytes are present.

value = ~value;

if (BitConverter.IsLittleEndian)
{
// Read first byte
value >>= 7;
uint allBytesUpToNowAreAscii = value & 1;
uint numAsciiBytes = allBytesUpToNowAreAscii;

// Read second byte
value >>= 8;
allBytesUpToNowAreAscii &= value;
numAsciiBytes += allBytesUpToNowAreAscii;

// Read third byte
value >>= 8;
allBytesUpToNowAreAscii &= value;
numAsciiBytes += allBytesUpToNowAreAscii;

return numAsciiBytes;
}
else
{
// BinaryPrimitives.ReverseEndianness is only implemented as an intrinsic on
// little-endian platforms, so using it in this big-endian path would be too
// expensive. Instead we'll just change how we perform the shifts.

// Read first byte
value = BitOperations.RotateLeft(value, 1);
uint allBytesUpToNowAreAscii = value & 1;
uint numAsciiBytes = allBytesUpToNowAreAscii;

// Read second byte
value = BitOperations.RotateLeft(value, 8);
allBytesUpToNowAreAscii &= value;
numAsciiBytes += allBytesUpToNowAreAscii;

// Read third byte
value = BitOperations.RotateLeft(value, 8);
allBytesUpToNowAreAscii &= value;
numAsciiBytes += allBytesUpToNowAreAscii;

return numAsciiBytes;
GrabYourPitchforks marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
110 changes: 14 additions & 96 deletions src/System.Private.CoreLib/shared/System/Text/ASCIIUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,20 @@ namespace System.Text
{
internal static partial class ASCIIUtility
{
/// <summary>
/// Returns <see langword="true"/> iff all bytes in <paramref name="value"/> are ASCII.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool AllBytesInUInt32AreAscii(uint value)
#if DEBUG
static ASCIIUtility()
{
return ((value & 0x80808080u) == 0);
Debug.Assert(sizeof(nint) == IntPtr.Size && nint.MinValue < 0, "nint is defined incorrectly.");
Debug.Assert(sizeof(nuint) == IntPtr.Size && nuint.MinValue == 0, "nuint is defined incorrectly.");
}
#endif // DEBUG

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool AllBytesInUInt64AreAscii(ulong value)
{
return ((value & 0x80808080_80808080ul) == 0);
// If the high bit of any byte is set, that byte is non-ASCII.

return ((value & UInt64HighBitsOnlyMask) == 0);
}

/// <summary>
Expand All @@ -54,56 +55,6 @@ private static bool AllCharsInUInt64AreAscii(ulong value)
return ((value & ~0x007F007F_007F007Ful) == 0);
}

/// <summary>
/// Given a 24-bit integer which represents a three-byte buffer read in machine endianness,
/// counts the number of consecutive ASCII bytes starting from the beginning of the buffer.
/// Returns a value 0 - 3, inclusive.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint CountNumberOfLeadingAsciiBytesFrom24BitInteger(uint value)
{
// This implementation seems to have better performance than tzcnt.

// The 'allBytesUpToNowAreAscii' DWORD uses bit twiddling to hold a 1 or a 0 depending
// on whether all processed bytes were ASCII. Then we accumulate all of the
// results to calculate how many consecutive ASCII bytes are present.

value = ~value;

if (BitConverter.IsLittleEndian)
{
// Read first byte
uint allBytesUpToNowAreAscii = (value >>= 7) & 1;
uint numAsciiBytes = allBytesUpToNowAreAscii;

// Read second byte
allBytesUpToNowAreAscii &= (value >>= 8);
numAsciiBytes += allBytesUpToNowAreAscii;

// Read third byte
allBytesUpToNowAreAscii &= (value >>= 8);
numAsciiBytes += allBytesUpToNowAreAscii;

return numAsciiBytes;
}
else
{
// Read first byte
uint allBytesUpToNowAreAscii = (value = ROL32(value, 1)) & 1;
uint numAsciiBytes = allBytesUpToNowAreAscii;

// Read second byte
allBytesUpToNowAreAscii &= (value = ROL32(value, 8));
numAsciiBytes += allBytesUpToNowAreAscii;

// Read third byte
allBytesUpToNowAreAscii &= (value = ROL32(value, 8));
numAsciiBytes += allBytesUpToNowAreAscii;

return numAsciiBytes;
}
}

/// <summary>
/// Given a DWORD which represents two packed chars in machine-endian order,
/// <see langword="true"/> iff the first char (in machine-endian order) is ASCII.
Expand Down Expand Up @@ -273,7 +224,7 @@ private static unsafe nuint GetIndexOfFirstNonAsciiByte_Default(byte* pBuffer, n
// we get to the high byte; or (b) all of the earlier bytes are ASCII, so the high byte must be
// non-ASCII. In both cases we only care about the low 24 bits.

pBuffer += CountNumberOfLeadingAsciiBytesFrom24BitInteger(currentUInt32);
pBuffer += CountNumberOfLeadingAsciiBytesFromUInt32WithSomeNonAsciiData(currentUInt32);
goto Finish;
}

Expand Down Expand Up @@ -435,7 +386,7 @@ private static unsafe nuint GetIndexOfFirstNonAsciiByte_Sse2(byte* pBuffer, nuin

uint currentDWord;
Debug.Assert(!AllBytesInUInt32AreAscii(currentDWord), "Shouldn't be here unless we see non-ASCII data.");
pBuffer += CountNumberOfLeadingAsciiBytesFrom24BitInteger(currentDWord);
pBuffer += CountNumberOfLeadingAsciiBytesFromUInt32WithSomeNonAsciiData(currentDWord);

goto Finish;

Expand All @@ -461,7 +412,7 @@ private static unsafe nuint GetIndexOfFirstNonAsciiByte_Sse2(byte* pBuffer, nuin
// Clear everything but the high bit of each byte, then tzcnt.
// Remember the / 8 at the end to convert bit count to byte count.

candidateUInt64 &= 0x80808080_80808080ul;
candidateUInt64 &= UInt64HighBitsOnlyMask;
pBuffer += (nuint)(Bmi1.X64.TrailingZeroCount(candidateUInt64) / 8);
goto Finish;
}
Expand Down Expand Up @@ -1395,17 +1346,7 @@ private static unsafe nuint NarrowUtf16ToAscii_Sse2(char* pUtf16Buffer, byte* pA
// Turn the 8 ASCII chars we just read into 8 ASCII bytes, then copy it to the destination.

Vector128<byte> asciiVector = Sse2.PackUnsignedSaturate(utf16VectorFirst, utf16VectorFirst);

if (Sse41.X64.IsSupported)
{
// Use PEXTRQ instruction if available, since it can extract from the vector directly to the destination address.
Unsafe.WriteUnaligned<ulong>(pAsciiBuffer, Sse41.X64.Extract(asciiVector.AsUInt64(), 0));
}
else
{
// Bounce this through a temporary register (with potential stack spillage) before writing to memory.
Unsafe.WriteUnaligned<ulong>(pAsciiBuffer, asciiVector.AsUInt64().GetElement(0));
}
Sse2.StoreLow((ulong*)pAsciiBuffer, asciiVector.AsUInt64()); // ulong* calculated here is UNALIGNED

nuint currentOffsetInElements = SizeOfVector128 / 2; // we processed 8 elements so far

Expand Down Expand Up @@ -1444,16 +1385,7 @@ private static unsafe nuint NarrowUtf16ToAscii_Sse2(char* pUtf16Buffer, byte* pA

// Turn the 8 ASCII chars we just read into 8 ASCII bytes, then copy it to the destination.
asciiVector = Sse2.PackUnsignedSaturate(utf16VectorFirst, utf16VectorFirst);

// See comments earlier in this method for information about how this works.
if (Sse41.X64.IsSupported)
{
Unsafe.WriteUnaligned<ulong>(pAsciiBuffer + currentOffsetInElements, Sse41.X64.Extract(asciiVector.AsUInt64(), 0));
}
else
{
Unsafe.WriteUnaligned<ulong>(pAsciiBuffer + currentOffsetInElements, asciiVector.AsUInt64().GetElement(0));
}
Sse2.StoreLow((ulong*)(pAsciiBuffer + currentOffsetInElements), asciiVector.AsUInt64()); // ulong* calculated here is UNALIGNED
}

// Calculate how many elements we wrote in order to get pAsciiBuffer to its next alignment
Expand Down Expand Up @@ -1529,26 +1461,12 @@ private static unsafe nuint NarrowUtf16ToAscii_Sse2(char* pUtf16Buffer, byte* pA

Debug.Assert(((nuint)pAsciiBuffer + currentOffsetInElements) % sizeof(ulong) == 0, "Destination should be ulong-aligned.");

// See comments earlier in this method for information about how this works.
if (Sse41.X64.IsSupported)
{
*(ulong*)(pAsciiBuffer + currentOffsetInElements) = Sse41.X64.Extract(asciiVector.AsUInt64(), 0);
}
else
{
*(ulong*)(pAsciiBuffer + currentOffsetInElements) = asciiVector.AsUInt64().GetElement(0);
}
Sse2.StoreLow((ulong*)(pAsciiBuffer + currentOffsetInElements), asciiVector.AsUInt64()); // ulong* calculated here is aligned
currentOffsetInElements += SizeOfVector128 / 2;

goto Finish;
}

/// <summary>
/// Rotates a <see cref="uint"/> left. The JIT is smart enough to turn this into a ROL / ROR instruction.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint ROL32(uint value, int shift) => (value << shift) | (value >> (32 - shift));

/// <summary>
/// Copies as many ASCII bytes (00..7F) as possible from <paramref name="pAsciiBuffer"/>
/// to <paramref name="pUtf16Buffer"/>, stopping when the first non-ASCII byte is encountered
Expand Down
Loading