Perf: Eliminate chatty virtual dispatches
This gives a speedup of approx. 20% (for overloads which take TextWriter) to 40% (for overloads which don't take TextWriter) for inputs in which at least one character requires encoding.
This commit is contained in:
parent
26cd8d51b6
commit
0dd3a49463
|
|
@ -117,17 +117,17 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes a scalar value as an HTML-encoded entity.
|
// Writes a scalar value as an HTML-encoded entity.
|
||||||
protected override void WriteEncodedScalar<T>(T output, Action<T, string> writeString, Action<T, char> writeChar, uint value)
|
protected override void WriteEncodedScalar(ref Writer writer, uint value)
|
||||||
{
|
{
|
||||||
if (value == (uint)'\"') { writeString(output, """); }
|
if (value == (uint)'\"') { writer.Write("""); }
|
||||||
else if (value == (uint)'&') { writeString(output, "&"); }
|
else if (value == (uint)'&') { writer.Write("&"); }
|
||||||
else if (value == (uint)'<') { writeString(output, "<"); }
|
else if (value == (uint)'<') { writer.Write("<"); }
|
||||||
else if (value == (uint)'>') { writeString(output, ">"); }
|
else if (value == (uint)'>') { writer.Write(">"); }
|
||||||
else { WriteEncodedScalarAsNumericEntity(output, writeChar, value); }
|
else { WriteEncodedScalarAsNumericEntity(ref writer, value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes a scalar value as an HTML-encoded numeric entity.
|
// Writes a scalar value as an HTML-encoded numeric entity.
|
||||||
private static void WriteEncodedScalarAsNumericEntity<T>(T output, Action<T, char> writeChar, uint value) where T : class
|
private static void WriteEncodedScalarAsNumericEntity(ref Writer writer, uint value)
|
||||||
{
|
{
|
||||||
// We're building the characters up in reverse
|
// We're building the characters up in reverse
|
||||||
char* chars = stackalloc char[8 /* "FFFFFFFF" */];
|
char* chars = stackalloc char[8 /* "FFFFFFFF" */];
|
||||||
|
|
@ -141,15 +141,15 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
|
||||||
} while (value != 0);
|
} while (value != 0);
|
||||||
|
|
||||||
// Finally, write out the HTML-encoded scalar value.
|
// Finally, write out the HTML-encoded scalar value.
|
||||||
writeChar(output, '&');
|
writer.Write('&');
|
||||||
writeChar(output, '#');
|
writer.Write('#');
|
||||||
writeChar(output, 'x');
|
writer.Write('x');
|
||||||
Debug.Assert(numCharsWritten > 0, "At least one character should've been written.");
|
Debug.Assert(numCharsWritten > 0, "At least one character should've been written.");
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
writeChar(output, chars[--numCharsWritten]);
|
writer.Write(chars[--numCharsWritten]);
|
||||||
} while (numCharsWritten != 0);
|
} while (numCharsWritten != 0);
|
||||||
writeChar(output, ';');
|
writer.Write(';');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
|
||||||
// See ECMA-262, Sec. 7.8.4, and ECMA-404, Sec. 9
|
// See ECMA-262, Sec. 7.8.4, and ECMA-404, Sec. 9
|
||||||
// http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4
|
// http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4
|
||||||
// http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf
|
// http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf
|
||||||
protected override void WriteEncodedScalar<T>(T output, Action<T, string> writeString, Action<T, char> writeChar, uint value)
|
protected override void WriteEncodedScalar(ref Writer writer, uint value)
|
||||||
{
|
{
|
||||||
// ECMA-262 allows encoding U+000B as "\v", but ECMA-404 does not.
|
// ECMA-262 allows encoding U+000B as "\v", but ECMA-404 does not.
|
||||||
// Both ECMA-262 and ECMA-404 allow encoding U+002F SOLIDUS as "\/".
|
// Both ECMA-262 and ECMA-404 allow encoding U+002F SOLIDUS as "\/".
|
||||||
|
|
@ -133,46 +133,46 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
|
||||||
// be written out as numeric entities for defense-in-depth.
|
// be written out as numeric entities for defense-in-depth.
|
||||||
// See UnicodeEncoderBase ctor comments for more info.
|
// See UnicodeEncoderBase ctor comments for more info.
|
||||||
|
|
||||||
if (value == (uint)'\b') { writeString(output, @"\b"); }
|
if (value == (uint)'\b') { writer.Write(@"\b"); }
|
||||||
else if (value == (uint)'\t') { writeString(output, @"\t"); }
|
else if (value == (uint)'\t') { writer.Write(@"\t"); }
|
||||||
else if (value == (uint)'\n') { writeString(output, @"\n"); }
|
else if (value == (uint)'\n') { writer.Write(@"\n"); }
|
||||||
else if (value == (uint)'\f') { writeString(output, @"\f"); }
|
else if (value == (uint)'\f') { writer.Write(@"\f"); }
|
||||||
else if (value == (uint)'\r') { writeString(output, @"\r"); }
|
else if (value == (uint)'\r') { writer.Write(@"\r"); }
|
||||||
else if (value == (uint)'/') { writeString(output, @"\/"); }
|
else if (value == (uint)'/') { writer.Write(@"\/"); }
|
||||||
else if (value == (uint)'\\') { writeString(output, @"\\"); }
|
else if (value == (uint)'\\') { writer.Write(@"\\"); }
|
||||||
else { WriteEncodedScalarAsNumericEntity(output, writeChar, value); }
|
else { WriteEncodedScalarAsNumericEntity(ref writer, value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes a scalar value as an JavaScript-escaped character (or sequence of characters).
|
// Writes a scalar value as an JavaScript-escaped character (or sequence of characters).
|
||||||
private static void WriteEncodedScalarAsNumericEntity<T>(T output, Action<T, char> writeChar, uint value) where T : class
|
private static void WriteEncodedScalarAsNumericEntity(ref Writer writer, uint value)
|
||||||
{
|
{
|
||||||
if (UnicodeHelpers.IsSupplementaryCodePoint((int)value))
|
if (UnicodeHelpers.IsSupplementaryCodePoint((int)value))
|
||||||
{
|
{
|
||||||
// Convert this back to UTF-16 and write out both characters.
|
// Convert this back to UTF-16 and write out both characters.
|
||||||
char leadingSurrogate, trailingSurrogate;
|
char leadingSurrogate, trailingSurrogate;
|
||||||
UnicodeHelpers.GetUtf16SurrogatePairFromAstralScalarValue((int)value, out leadingSurrogate, out trailingSurrogate);
|
UnicodeHelpers.GetUtf16SurrogatePairFromAstralScalarValue((int)value, out leadingSurrogate, out trailingSurrogate);
|
||||||
WriteEncodedSingleCharacter(output, writeChar, leadingSurrogate);
|
WriteEncodedSingleCharacter(ref writer, leadingSurrogate);
|
||||||
WriteEncodedSingleCharacter(output, writeChar, trailingSurrogate);
|
WriteEncodedSingleCharacter(ref writer, trailingSurrogate);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// This is only a single character.
|
// This is only a single character.
|
||||||
WriteEncodedSingleCharacter(output, writeChar, value);
|
WriteEncodedSingleCharacter(ref writer, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes an encoded scalar value (in the BMP) as a JavaScript-escaped character.
|
// Writes an encoded scalar value (in the BMP) as a JavaScript-escaped character.
|
||||||
private static void WriteEncodedSingleCharacter<T>(T output, Action<T, char> writeChar, uint value) where T : class
|
private static void WriteEncodedSingleCharacter(ref Writer writer, uint value)
|
||||||
{
|
{
|
||||||
Debug.Assert(!UnicodeHelpers.IsSupplementaryCodePoint((int)value), "The incoming value should've been in the BMP.");
|
Debug.Assert(!UnicodeHelpers.IsSupplementaryCodePoint((int)value), "The incoming value should've been in the BMP.");
|
||||||
|
|
||||||
// Encode this as 6 chars "\uFFFF".
|
// Encode this as 6 chars "\uFFFF".
|
||||||
writeChar(output, '\\');
|
writer.Write('\\');
|
||||||
writeChar(output, 'u');
|
writer.Write('u');
|
||||||
writeChar(output, HexUtil.IntToChar(value >> 12));
|
writer.Write(HexUtil.IntToChar(value >> 12));
|
||||||
writeChar(output, HexUtil.IntToChar((value >> 8) & 0xFU));
|
writer.Write(HexUtil.IntToChar((value >> 8) & 0xFU));
|
||||||
writeChar(output, HexUtil.IntToChar((value >> 4) & 0xFU));
|
writer.Write(HexUtil.IntToChar((value >> 4) & 0xFU));
|
||||||
writeChar(output, HexUtil.IntToChar(value & 0xFU));
|
writer.Write(HexUtil.IntToChar(value & 0xFU));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,6 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
|
||||||
{
|
{
|
||||||
internal unsafe abstract class UnicodeEncoderBase
|
internal unsafe abstract class UnicodeEncoderBase
|
||||||
{
|
{
|
||||||
// Stubs for appending data to a TextWriter or StringBuilder
|
|
||||||
private static readonly Action<StringBuilder, char> _appendCharToStringBuilderStub = PrepareDelegate((Action<StringBuilder, char>)AppendToStringBuilder);
|
|
||||||
private static readonly Action<TextWriter, char> _appendCharToTextWriterStub = PrepareDelegate((Action<TextWriter, char>)AppendToTextWriter);
|
|
||||||
private static readonly Action<StringBuilder, string> _appendStringToStringBuilderStub = PrepareDelegate((Action<StringBuilder, string>)AppendToStringBuilder);
|
|
||||||
private static readonly Action<TextWriter, string> _appendStringToTextWriterStub = PrepareDelegate((Action<TextWriter, string>)AppendToTextWriter);
|
|
||||||
|
|
||||||
// A bitmap of characters which are allowed to be returned unescaped.
|
// A bitmap of characters which are allowed to be returned unescaped.
|
||||||
private readonly uint[] _allowedCharsBitmap = new uint[0x10000 / 32];
|
private readonly uint[] _allowedCharsBitmap = new uint[0x10000 / 32];
|
||||||
|
|
||||||
|
|
@ -77,26 +71,6 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
|
||||||
_allowedCharsBitmap[index] |= 0x1U << offset;
|
_allowedCharsBitmap[index] |= 0x1U << offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AppendToStringBuilder(StringBuilder builder, char value)
|
|
||||||
{
|
|
||||||
builder.Append(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AppendToStringBuilder(StringBuilder builder, string value)
|
|
||||||
{
|
|
||||||
builder.Append(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AppendToTextWriter(TextWriter writer, char value)
|
|
||||||
{
|
|
||||||
writer.Write(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AppendToTextWriter(TextWriter writer, string value)
|
|
||||||
{
|
|
||||||
writer.Write(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marks a character as forbidden (must be returned encoded)
|
// Marks a character as forbidden (must be returned encoded)
|
||||||
protected void ForbidCharacter(char c)
|
protected void ForbidCharacter(char c)
|
||||||
{
|
{
|
||||||
|
|
@ -212,19 +186,21 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
|
||||||
// Allocate the StringBuilder with the first (known to not require encoding) part of the input string,
|
// Allocate the StringBuilder with the first (known to not require encoding) part of the input string,
|
||||||
// then begin encoding from the last (potentially requiring encoding) part of the input string.
|
// then begin encoding from the last (potentially requiring encoding) part of the input string.
|
||||||
StringBuilder builder = new StringBuilder(input, 0, idxOfFirstCharWhichRequiresEncoding, sbCapacity);
|
StringBuilder builder = new StringBuilder(input, 0, idxOfFirstCharWhichRequiresEncoding, sbCapacity);
|
||||||
|
Writer writer = new Writer(builder);
|
||||||
fixed (char* pInput = input)
|
fixed (char* pInput = input)
|
||||||
{
|
{
|
||||||
EncodeCore(builder, _appendStringToStringBuilderStub, _appendCharToStringBuilderStub, &pInput[idxOfFirstCharWhichRequiresEncoding], (uint)numCharsWhichMayRequireEncoding);
|
EncodeCore(ref writer, &pInput[idxOfFirstCharWhichRequiresEncoding], (uint)numCharsWhichMayRequireEncoding);
|
||||||
}
|
}
|
||||||
return builder.ToString();
|
return builder.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EncodeCore(char* input, uint charsRemaining, TextWriter output)
|
private void EncodeCore(char* input, uint charsRemaining, TextWriter output)
|
||||||
{
|
{
|
||||||
EncodeCore(output, _appendStringToTextWriterStub, _appendCharToTextWriterStub, input, charsRemaining);
|
Writer writer = new Writer(output);
|
||||||
|
EncodeCore(ref writer, input, charsRemaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EncodeCore<T>(T output, Action<T, string> writeString, Action<T, char> writeChar, char* input, uint charsRemaining) where T : class
|
private void EncodeCore(ref Writer writer, char* input, uint charsRemaining)
|
||||||
{
|
{
|
||||||
while (charsRemaining != 0)
|
while (charsRemaining != 0)
|
||||||
{
|
{
|
||||||
|
|
@ -232,7 +208,7 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
|
||||||
if (UnicodeHelpers.IsSupplementaryCodePoint(nextScalar))
|
if (UnicodeHelpers.IsSupplementaryCodePoint(nextScalar))
|
||||||
{
|
{
|
||||||
// Supplementary characters should always be encoded numerically.
|
// Supplementary characters should always be encoded numerically.
|
||||||
WriteEncodedScalar(output, writeString, writeChar, (uint)nextScalar);
|
WriteEncodedScalar(ref writer, (uint)nextScalar);
|
||||||
|
|
||||||
// We consume two UTF-16 characters for a single supplementary character.
|
// We consume two UTF-16 characters for a single supplementary character.
|
||||||
input += 2;
|
input += 2;
|
||||||
|
|
@ -246,11 +222,11 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
|
||||||
char c = (char)nextScalar;
|
char c = (char)nextScalar;
|
||||||
if (IsCharacterAllowed(c))
|
if (IsCharacterAllowed(c))
|
||||||
{
|
{
|
||||||
writeChar(output, c);
|
writer.Write(c);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
WriteEncodedScalar(output, writeString, writeChar, (uint)nextScalar);
|
WriteEncodedScalar(ref writer, (uint)nextScalar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -278,22 +254,6 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
|
||||||
return ((_allowedCharsBitmap[index] >> offset) & 0x1U) != 0;
|
return ((_allowedCharsBitmap[index] >> offset) & 0x1U) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static T PrepareDelegate<T>(T @del) where T : class
|
|
||||||
{
|
|
||||||
#if ASPNETCORE50
|
|
||||||
// RuntimeHelpers.PrepareMethod doesn't exist on CoreCLR, so we'll depend
|
|
||||||
// on cross-gen for performance optimizations.
|
|
||||||
return del;
|
|
||||||
#else
|
|
||||||
// We prepare the method ahead of time to ensure that it's JITted before
|
|
||||||
// the delegate is constructed; this allows the delegate to point straight
|
|
||||||
// to the processor code rather than to the prestub dispatch code.
|
|
||||||
Delegate castDel = (Delegate)(object)del;
|
|
||||||
RuntimeHelpers.PrepareMethod(castDel.Method.MethodHandle);
|
|
||||||
return (T)(object)castDel.Method.CreateDelegate(typeof(T));
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ValidateInputs(int startIndex, int charCount, int actualInputLength)
|
private static void ValidateInputs(int startIndex, int charCount, int actualInputLength)
|
||||||
{
|
{
|
||||||
if (startIndex < 0 || startIndex > actualInputLength)
|
if (startIndex < 0 || startIndex > actualInputLength)
|
||||||
|
|
@ -306,6 +266,55 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void WriteEncodedScalar<T>(T output, Action<T, string> writeString, Action<T, char> writeChar, uint value) where T : class;
|
protected abstract void WriteEncodedScalar(ref Writer writer, uint value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides an abstraction over both StringBuilder and TextWriter.
|
||||||
|
/// Declared as a struct so we can allocate on the stack and pass by
|
||||||
|
/// reference. Eliminates chatty virtual dispatches on hot paths.
|
||||||
|
/// </summary>
|
||||||
|
protected struct Writer
|
||||||
|
{
|
||||||
|
private readonly StringBuilder _innerBuilder;
|
||||||
|
private readonly TextWriter _innerWriter;
|
||||||
|
|
||||||
|
public Writer(StringBuilder innerBuilder)
|
||||||
|
{
|
||||||
|
_innerBuilder = innerBuilder;
|
||||||
|
_innerWriter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Writer(TextWriter innerWriter)
|
||||||
|
{
|
||||||
|
_innerBuilder = null;
|
||||||
|
_innerWriter = innerWriter;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void Write(char value)
|
||||||
|
{
|
||||||
|
if (_innerBuilder != null)
|
||||||
|
{
|
||||||
|
_innerBuilder.Append(value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_innerWriter.Write(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void Write(string value)
|
||||||
|
{
|
||||||
|
if (_innerBuilder != null)
|
||||||
|
{
|
||||||
|
_innerBuilder.Append(value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_innerWriter.Write(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,16 +160,16 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes a scalar value as a percent-encoded sequence of UTF8 bytes, per RFC 3987.
|
// Writes a scalar value as a percent-encoded sequence of UTF8 bytes, per RFC 3987.
|
||||||
protected override void WriteEncodedScalar<T>(T output, Action<T, string> writeString, Action<T, char> writeChar, uint value)
|
protected override void WriteEncodedScalar(ref Writer writer, uint value)
|
||||||
{
|
{
|
||||||
uint asUtf8 = (uint)UnicodeHelpers.GetUtf8RepresentationForScalarValue(value);
|
uint asUtf8 = (uint)UnicodeHelpers.GetUtf8RepresentationForScalarValue(value);
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
char highNibble, lowNibble;
|
char highNibble, lowNibble;
|
||||||
HexUtil.WriteHexEncodedByte((byte)asUtf8, out highNibble, out lowNibble);
|
HexUtil.WriteHexEncodedByte((byte)asUtf8, out highNibble, out lowNibble);
|
||||||
writeChar(output, '%');
|
writer.Write('%');
|
||||||
writeChar(output, highNibble);
|
writer.Write(highNibble);
|
||||||
writeChar(output, lowNibble);
|
writer.Write(lowNibble);
|
||||||
} while ((asUtf8 >>= 8) != 0);
|
} while ((asUtf8 >>= 8) != 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue