From 7381086817fbb0bae64ffae14a06e54cc4100e95 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Thu, 24 Oct 2024 20:20:49 +0300 Subject: [PATCH] Optimize ValueStringBuilder and some character checks (#1986) --- Jint/Extensions/Character.cs | 12 ++-- Jint/Native/Global/GlobalObject.cs | 64 +++++++++++-------- Jint/Native/Json/JsonSerializer.cs | 4 +- Jint/Native/Number/NumberPrototype.cs | 4 +- Jint/Native/String/StringPrototype.cs | 4 -- .../TypedArrayConstructor.Uint8Array.cs | 11 ++-- Jint/Pooling/ValueStringBuilder.cs | 39 +++++++++++ Jint/Runtime/CallStack/JintCallStack.cs | 5 +- Jint/Runtime/Interop/DefaultTypeConverter.cs | 2 +- 9 files changed, 95 insertions(+), 50 deletions(-) diff --git a/Jint/Extensions/Character.cs b/Jint/Extensions/Character.cs index d845fe9beb..4d65aa1b40 100644 --- a/Jint/Extensions/Character.cs +++ b/Jint/Extensions/Character.cs @@ -6,11 +6,13 @@ namespace Jint.Extensions; internal static class Character { [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsInRange(this char c, ushort min, ushort max) - { - Debug.Assert(min <= max); - return c - (uint) min <= max - (uint) min; - } + public static bool IsInRange(this char c, ushort min, ushort max) => (uint)(c - min) <= (uint)(max - min); + + /// + /// https://tc39.es/ecma262/#ASCII-word-characters + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAsciiWordCharacter(this char c) => c == '_' || c.IsDecimalDigit() || c.IsInRange('a', 'z') || c.IsInRange('A', 'Z'); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsOctalDigit(this char c) => c.IsInRange('0', '7'); diff --git a/Jint/Native/Global/GlobalObject.cs b/Jint/Native/Global/GlobalObject.cs index ae0859ae7a..d64b5c84c4 100644 --- a/Jint/Native/Global/GlobalObject.cs +++ b/Jint/Native/Global/GlobalObject.cs @@ -1,6 +1,5 @@ using System.Buffers; using System.Globalization; -using System.Linq; using System.Runtime.CompilerServices; using System.Text; using Jint.Extensions; @@ -274,7 +273,7 @@ private static JsValue IsFinite(JsValue thisObject, JsValue[] arguments) } private const string UriReservedString = ";/?:@&=+$,"; - private const string UriUnescapedString = "-_.!~*'()"; + private const string UriUnescapedString = "-.!~*'()"; private static readonly SearchValues UriUnescaped = SearchValues.Create(UriUnescapedString); private static readonly SearchValues UnescapedUriSet = SearchValues.Create(UriReservedString + UriUnescapedString + '#'); private static readonly SearchValues ReservedUriSet = SearchValues.Create(UriReservedString + '#'); @@ -303,20 +302,16 @@ private JsValue EncodeUriComponent(JsValue thisObject, JsValue[] arguments) private JsValue Encode(string uriString, SearchValues unescapedUriSet) { - const string HexaMap = "0123456789ABCDEF"; - var strLen = uriString.Length; - - _stringBuilder.EnsureCapacity(uriString.Length); - _stringBuilder.Clear(); + var builder = new ValueStringBuilder(uriString.Length); Span buffer = stackalloc byte[4]; for (var k = 0; k < strLen; k++) { var c = uriString[k]; - if (c is >= 'a' and <= 'z' || c is >= 'A' and <= 'Z' || c is >= '0' and <= '9' || unescapedUriSet.Contains(c)) + if (c.IsAsciiWordCharacter() || unescapedUriSet.Contains(c)) { - _stringBuilder.Append(c); + builder.Append(c); } else { @@ -386,15 +381,13 @@ private JsValue Encode(string uriString, SearchValues unescapedUriSet) for (var i = 0; i < length; i++) { - var octet = buffer[i]; - var x1 = HexaMap[octet / 16]; - var x2 = HexaMap[octet % 16]; - _stringBuilder.Append('%').Append(x1).Append(x2); + builder.Append('%'); + builder.AppendHex(buffer[i]); } } } - return _stringBuilder.ToString(); + return builder.ToString(); uriError: _engine.SignalError(ExceptionHelper.CreateUriError(_realm, "URI malformed")); @@ -583,10 +576,8 @@ private static bool IsDigit(char c, int radix, out int result) return tmp < radix; } - private static readonly SearchValues EscapeAllowList = SearchValues.Create("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_ + -./"); - /// - /// http://www.ecma-international.org/ecma-262/5.1/#sec-B.2.1 + /// https://tc39.es/ecma262/#sec-escape-string /// private JsValue Escape(JsValue thisObject, JsValue[] arguments) { @@ -600,7 +591,7 @@ private JsValue Escape(JsValue thisObject, JsValue[] arguments) for (var k = 0; k < strLen; k++) { var c = uriString[k]; - if (EscapeAllowList.Contains(c)) + if (c.IsAsciiWordCharacter() || c == '@' || c == '*' || c == '+' || c == '-' || c == '.' || c == '/') { _stringBuilder.Append(c); } @@ -636,19 +627,14 @@ private JsValue Unescape(JsValue thisObject, JsValue[] arguments) { if (k <= strLen - 6 && uriString[k + 1] == 'u' - && uriString.Skip(k + 2).Take(4).All(IsValidHexaChar)) + && AreValidHexChars(uriString.AsSpan(k + 2, 4))) { - var joined = string.Join(string.Empty, uriString.Skip(k + 2).Take(4)); - c = (char) int.Parse(joined, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture); - + c = ParseHexString(uriString.AsSpan(k + 2, 4)); k += 5; } - else if (k <= strLen - 3 - && uriString.Skip(k + 1).Take(2).All(IsValidHexaChar)) + else if (k <= strLen - 3 && AreValidHexChars(uriString.AsSpan(k + 1, 2))) { - var joined = string.Join(string.Empty, uriString.Skip(k + 1).Take(2)); - c = (char) int.Parse(joined, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture); - + c = ParseHexString(uriString.AsSpan(k + 1, 2)); k += 2; } } @@ -656,6 +642,30 @@ private JsValue Unescape(JsValue thisObject, JsValue[] arguments) } return _stringBuilder.ToString(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool AreValidHexChars(ReadOnlySpan input) + { + foreach (var c in input) + { + if (!IsValidHexaChar(c)) + { + return false; + } + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static char ParseHexString(ReadOnlySpan input) + { +#if NET6_0_OR_GREATER + return (char) int.Parse(input, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture); +#else + return (char) int.Parse(input.ToString(), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture); +#endif + } } // optimized versions with string parameter and without virtual dispatch for global environment usage diff --git a/Jint/Native/Json/JsonSerializer.cs b/Jint/Native/Json/JsonSerializer.cs index ca7c413695..d9d7e28fd7 100644 --- a/Jint/Native/Json/JsonSerializer.cs +++ b/Jint/Native/Json/JsonSerializer.cs @@ -184,7 +184,7 @@ private SerializeResult SerializeJSONProperty(JsValue key, JsValue holder, ref V if (value.IsInteger()) { - json.Append(((long) doubleValue).ToString(CultureInfo.InvariantCulture)); + json.Append((long) doubleValue); return SerializeResult.NotUndefined; } @@ -193,7 +193,7 @@ private SerializeResult SerializeJSONProperty(JsValue key, JsValue holder, ref V { if (TypeConverter.CanBeStringifiedAsLong(doubleValue)) { - json.Append(((long) doubleValue).ToString(CultureInfo.InvariantCulture)); + json.Append((long) doubleValue); return SerializeResult.NotUndefined; } diff --git a/Jint/Native/Number/NumberPrototype.cs b/Jint/Native/Number/NumberPrototype.cs index 8697b60c64..43d9d84d0d 100644 --- a/Jint/Native/Number/NumberPrototype.cs +++ b/Jint/Native/Number/NumberPrototype.cs @@ -353,7 +353,7 @@ private static string CreateExponentialRepresentation( sb.Append('e'); sb.Append(negativeExponent ? '-' : '+'); - sb.Append(exponent.ToString(CultureInfo.InvariantCulture)); + sb.Append(exponent); return sb.ToString(); } @@ -528,7 +528,7 @@ internal static string ToNumberString(double m) exponent = -exponent; } - stringBuilder.Append(exponent.ToString(CultureInfo.InvariantCulture)); + stringBuilder.Append(exponent); } return stringBuilder.ToString(); diff --git a/Jint/Native/String/StringPrototype.cs b/Jint/Native/String/StringPrototype.cs index fe83439128..c1a13ec06c 100644 --- a/Jint/Native/String/StringPrototype.cs +++ b/Jint/Native/String/StringPrototype.cs @@ -700,11 +700,7 @@ static int StringIndexOf(string s, string search, int fromIndex) if (endOfLastMatch < thisString.Length) { -#if NETFRAMEWORK result.Append(thisString.AsSpan(endOfLastMatch)); -#else - result.Append(thisString[endOfLastMatch..]); -#endif } return result.ToString(); diff --git a/Jint/Native/TypedArray/TypedArrayConstructor.Uint8Array.cs b/Jint/Native/TypedArray/TypedArrayConstructor.Uint8Array.cs index 08ad54437d..64e98a2be4 100644 --- a/Jint/Native/TypedArray/TypedArrayConstructor.Uint8Array.cs +++ b/Jint/Native/TypedArray/TypedArrayConstructor.Uint8Array.cs @@ -92,8 +92,6 @@ internal static JsString GetAndValidateAlphabetOption(Engine engine, ObjectInsta internal readonly record struct FromEncodingResult(byte[] Bytes, JavaScriptException? Error, int Read); - private static readonly SearchValues Base64Alphabet = SearchValues.Create("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"); - internal static FromEncodingResult FromBase64(Engine engine, string input, string alphabet, string lastChunkHandling, uint maxLength = uint.MaxValue) { if (maxLength == 0) @@ -202,7 +200,10 @@ internal static FromEncodingResult FromBase64(Engine engine, string input, strin } } - if (!Base64Alphabet.Contains(currentChar)) + if (!currentChar.IsDecimalDigit() + && !char.ToLowerInvariant(currentChar).IsInRange('a', 'z') + && currentChar != '+' + && currentChar != '/') { return new FromEncodingResult(bytes.ToArray(), ExceptionHelper.CreateSyntaxError(engine.Realm, "Invalid base64 character."), read); } @@ -308,8 +309,6 @@ private JsTypedArray FromHex(JsValue thisObject, JsValue[] arguments) return ta; } - private static readonly SearchValues HexAlphabet = SearchValues.Create("0123456789abcdefABCDEF"); - internal static FromEncodingResult FromHex(Engine engine, string s, uint maxLength = int.MaxValue) { var length = s.Length; @@ -325,7 +324,7 @@ internal static FromEncodingResult FromHex(Engine engine, string s, uint maxLeng while (read < length && byteIndex < maxLength) { var hexits = s.AsSpan(read, 2); - if (!HexAlphabet.Contains(hexits[0]) || !HexAlphabet.Contains(hexits[1])) + if (!hexits[0].IsHexDigit() || !hexits[1].IsHexDigit()) { return new FromEncodingResult(bytes.AsSpan(0, byteIndex).ToArray(), ExceptionHelper.CreateSyntaxError(engine.Realm, "Invalid hex value"), read); } diff --git a/Jint/Pooling/ValueStringBuilder.cs b/Jint/Pooling/ValueStringBuilder.cs index f5bd679329..4252c3518c 100644 --- a/Jint/Pooling/ValueStringBuilder.cs +++ b/Jint/Pooling/ValueStringBuilder.cs @@ -188,6 +188,18 @@ public void Append(char c) } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AppendHex(byte b) + { + const string Map = "0123456789ABCDEF"; + Span data = stackalloc char[] + { + Map[b / 16], + Map[b % 16], + }; + Append(data); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(string? s) { @@ -208,6 +220,33 @@ public void Append(string? s) } } +#if NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(T value) where T : ISpanFormattable + { + if (value.TryFormat(_chars.Slice(_pos), out var charsWritten, format: default, provider: null)) + { + _pos += charsWritten; + } + else + { + Append(value.ToString()); + } + } +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(int value) + { + Append(value.ToString(System.Globalization.CultureInfo.InvariantCulture)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(long value) + { + Append(value.ToString(System.Globalization.CultureInfo.InvariantCulture)); + } +#endif + private void AppendSlow(string s) { int pos = _pos; diff --git a/Jint/Runtime/CallStack/JintCallStack.cs b/Jint/Runtime/CallStack/JintCallStack.cs index 087ed0074a..b52fedaa89 100644 --- a/Jint/Runtime/CallStack/JintCallStack.cs +++ b/Jint/Runtime/CallStack/JintCallStack.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Linq; using System.Text; using Jint.Collections; @@ -151,9 +150,9 @@ static void AppendLocation( sb.Append(' '); sb.Append(loc.SourceFile); sb.Append(':'); - sb.Append(loc.End.Line.ToString(CultureInfo.InvariantCulture)); + sb.Append(loc.End.Line); sb.Append(':'); - sb.Append((loc.Start.Column + 1).ToString(CultureInfo.InvariantCulture)); // report column number instead of index + sb.Append(loc.Start.Column + 1); // report column number instead of index sb.Append(System.Environment.NewLine); } diff --git a/Jint/Runtime/Interop/DefaultTypeConverter.cs b/Jint/Runtime/Interop/DefaultTypeConverter.cs index 48b8ae53ce..5cbcec842d 100644 --- a/Jint/Runtime/Interop/DefaultTypeConverter.cs +++ b/Jint/Runtime/Interop/DefaultTypeConverter.cs @@ -201,7 +201,7 @@ private bool TryConvert( foreach (var constructor in constructors) { var parameterInfos = constructor.GetParameters(); - if (parameterInfos.All(static p => p.IsOptional) && constructor.IsPublic) + if (Array.TrueForAll(parameterInfos, static p => p.IsOptional) && constructor.IsPublic) { constructorParameters = new object[parameterInfos.Length]; found = true;