瀏覽代碼

Optimize ValueStringBuilder and some character checks (#1986)

Marko Lahma 9 月之前
父節點
當前提交
7381086817

+ 7 - 5
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);
+
+    /// <summary>
+    /// https://tc39.es/ecma262/#ASCII-word-characters
+    /// </summary>
+    [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');

+ 37 - 27
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 @@ public sealed partial class GlobalObject : ObjectInstance
     }
 
     private const string UriReservedString = ";/?:@&=+$,";
-    private const string UriUnescapedString = "-_.!~*'()";
+    private const string UriUnescapedString = "-.!~*'()";
     private static readonly SearchValues<char> UriUnescaped = SearchValues.Create(UriUnescapedString);
     private static readonly SearchValues<char> UnescapedUriSet = SearchValues.Create(UriReservedString + UriUnescapedString + '#');
     private static readonly SearchValues<char> ReservedUriSet = SearchValues.Create(UriReservedString + '#');
@@ -303,20 +302,16 @@ public sealed partial class GlobalObject : ObjectInstance
 
     private JsValue Encode(string uriString, SearchValues<char> unescapedUriSet)
     {
-        const string HexaMap = "0123456789ABCDEF";
-
         var strLen = uriString.Length;
-
-        _stringBuilder.EnsureCapacity(uriString.Length);
-        _stringBuilder.Clear();
+        var builder = new ValueStringBuilder(uriString.Length);
         Span<byte> 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 @@ public sealed partial class GlobalObject : ObjectInstance
 
                 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 @@ public sealed partial class GlobalObject : ObjectInstance
         return tmp < radix;
     }
 
-    private static readonly SearchValues<char> EscapeAllowList = SearchValues.Create("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_ + -./");
-
     /// <summary>
-    /// http://www.ecma-international.org/ecma-262/5.1/#sec-B.2.1
+    /// https://tc39.es/ecma262/#sec-escape-string
     /// </summary>
     private JsValue Escape(JsValue thisObject, JsValue[] arguments)
     {
@@ -600,7 +591,7 @@ public sealed partial class GlobalObject : ObjectInstance
         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 @@ public sealed partial class GlobalObject : ObjectInstance
             {
                 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 @@ public sealed partial class GlobalObject : ObjectInstance
         }
 
         return _stringBuilder.ToString();
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        static bool AreValidHexChars(ReadOnlySpan<char> input)
+        {
+            foreach (var c in input)
+            {
+                if (!IsValidHexaChar(c))
+                {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        static char ParseHexString(ReadOnlySpan<char> 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

+ 2 - 2
Jint/Native/Json/JsonSerializer.cs

@@ -184,7 +184,7 @@ public sealed class JsonSerializer
 
             if (value.IsInteger())
             {
-                json.Append(((long) doubleValue).ToString(CultureInfo.InvariantCulture));
+                json.Append((long) doubleValue);
                 return SerializeResult.NotUndefined;
             }
 
@@ -193,7 +193,7 @@ public sealed class JsonSerializer
             {
                 if (TypeConverter.CanBeStringifiedAsLong(doubleValue))
                 {
-                    json.Append(((long) doubleValue).ToString(CultureInfo.InvariantCulture));
+                    json.Append((long) doubleValue);
                     return SerializeResult.NotUndefined;
                 }
 

+ 2 - 2
Jint/Native/Number/NumberPrototype.cs

@@ -353,7 +353,7 @@ internal sealed class NumberPrototype : NumberInstance
 
         sb.Append('e');
         sb.Append(negativeExponent ? '-' : '+');
-        sb.Append(exponent.ToString(CultureInfo.InvariantCulture));
+        sb.Append(exponent);
 
         return sb.ToString();
     }
@@ -528,7 +528,7 @@ internal sealed class NumberPrototype : NumberInstance
                 exponent = -exponent;
             }
 
-            stringBuilder.Append(exponent.ToString(CultureInfo.InvariantCulture));
+            stringBuilder.Append(exponent);
         }
 
         return stringBuilder.ToString();

+ 0 - 4
Jint/Native/String/StringPrototype.cs

@@ -700,11 +700,7 @@ internal sealed class StringPrototype : StringInstance
 
         if (endOfLastMatch < thisString.Length)
         {
-#if NETFRAMEWORK
             result.Append(thisString.AsSpan(endOfLastMatch));
-#else
-                result.Append(thisString[endOfLastMatch..]);
-#endif
         }
 
         return result.ToString();

+ 5 - 6
Jint/Native/TypedArray/TypedArrayConstructor.Uint8Array.cs

@@ -92,8 +92,6 @@ public sealed class Uint8ArrayConstructor : TypedArrayConstructor
 
     internal readonly record struct FromEncodingResult(byte[] Bytes, JavaScriptException? Error, int Read);
 
-    private static readonly SearchValues<char> 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 @@ public sealed class Uint8ArrayConstructor : TypedArrayConstructor
                 }
             }
 
-            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 @@ public sealed class Uint8ArrayConstructor : TypedArrayConstructor
         return ta;
     }
 
-    private static readonly SearchValues<char> HexAlphabet = SearchValues.Create("0123456789abcdefABCDEF");
-
     internal static FromEncodingResult FromHex(Engine engine, string s, uint maxLength = int.MaxValue)
     {
         var length = s.Length;
@@ -325,7 +324,7 @@ public sealed class Uint8ArrayConstructor : TypedArrayConstructor
         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);
             }

+ 39 - 0
Jint/Pooling/ValueStringBuilder.cs

@@ -188,6 +188,18 @@ internal ref struct ValueStringBuilder
         }
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void AppendHex(byte b)
+    {
+        const string Map = "0123456789ABCDEF";
+        Span<char> data = stackalloc char[]
+        {
+            Map[b / 16],
+            Map[b % 16],
+        };
+        Append(data);
+    }
+
     [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void Append(string? s)
     {
@@ -208,6 +220,33 @@ internal ref struct ValueStringBuilder
         }
     }
 
+#if NET6_0_OR_GREATER
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Append<T>(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;

+ 2 - 3
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 @@ internal sealed class JintCallStack
             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);
         }
 

+ 1 - 1
Jint/Runtime/Interop/DefaultTypeConverter.cs

@@ -201,7 +201,7 @@ public class DefaultTypeConverter : ITypeConverter
                     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;