Skip to content

Instantly share code, notes, and snippets.

@layomia
Forked from ahsonkhan/JsonEscapingCheck.md
Created November 30, 2020 07:26
Show Gist options
  • Save layomia/7d0eb0334f86b2d3a84760ad922a115f to your computer and use it in GitHub Desktop.
Save layomia/7d0eb0334f86b2d3a84760ad922a115f to your computer and use it in GitHub Desktop.
Optimize the check to see whether a JSON string contains characters that need to be escaped.

The default behavior of the System.Text.Json JsonSerializer and Utf8JsonWriter follows the behavior of JavascriptEncoder.Default, which means the following character sets are escaped:

  • Any non-ascii character
  • Characters required escaping based on the JSON RFC
  • HTML-specific ascii characters

Current Approach

The current approach involves going through the list of characters, one at a time, and check whether that character needs escaping. We special case the "null" encoder (which is the default when the user doesn't specify their own) and use a 256 byte bit-mask to indicate which character needs to be escaped. https://github.com/dotnet/corefx/blob/52d63157c78c31816b81be1599a5dacf96b5e5ca/src/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.Escaping.cs#L82-L109

New Approach Chosen

Use the Sse2 hardware intrinsics (where supported) to process 8 characters at a time and if any of them require escaping, return at which index the first character requiring escaping occurs, using TrailingZeroCount. For input strings that are less than 8 characters, fall back to the original approach of processing one character at a time.

public static int NeedsEscapingIntrinsics(ReadOnlySpan<char> value, JavaScriptEncoder encoder)
{
    fixed (char* ptr = value)
    {
        int idx = 0;

        // Some implementations of JavascriptEncoder.FindFirstCharacterToEncode may not accept
        // null pointers and gaurd against that. Hence, check up-front and fall down to return -1.
        if (encoder != null && !value.IsEmpty)
        {
            idx = encoder.FindFirstCharacterToEncode(ptr, value.Length);
            goto Return;
        }

        short* startingAddress = (short*)ptr;
        while (value.Length - 8 >= idx)
        {
            Vector128<short> sourceValue = Sse2.LoadVector128(startingAddress);

            Vector128<short> mask = CreateEscapingMask(sourceValue);

            int index = Sse2.MoveMask(mask.AsByte());
            // TrailingZeroCount is relatively expensive, avoid it if possible.
            if (index != 0)
            {
                idx += BitOperations.TrailingZeroCount(index) >> 1;
                goto Return;
            }
            idx += 8;
            startingAddress += 8;
        }

        for (; idx < value.Length; idx++)
        {
            if (NeedsEscaping(*(ptr + idx)))
            {
                goto Return;
            }
        }

        idx = -1; // all characters allowed

    Return:
        return idx;
    }
}

public const int LastAsciiCharacter = 0x7F;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool NeedsEscaping(char value) => value > LastAsciiCharacter || AllowList[value] == 0;

private static ReadOnlySpan<byte> AllowList => new byte[byte.MaxValue + 1]
{
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // U+0000..U+000F
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // U+0010..U+001F
    1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, // U+0020..U+002F
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, // U+0030..U+003F
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // U+0040..U+004F
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // U+0050..U+005F
    0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // U+0060..U+006F
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // U+0070..U+007F

    // Also include the ranges from U+0080 to U+00FF for performance to avoid UTF8 code from checking boundary.
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // U+00F0..U+00FF
};

private static readonly Vector128<short> Mask_UInt16_0x20 = Vector128.Create((short)0x20);

private static readonly Vector128<short> Mask_UInt16_0x22 = Vector128.Create((short)0x22);
private static readonly Vector128<short> Mask_UInt16_0x26 = Vector128.Create((short)0x26);
private static readonly Vector128<short> Mask_UInt16_0x27 = Vector128.Create((short)0x27);
private static readonly Vector128<short> Mask_UInt16_0x2B = Vector128.Create((short)0x2B);
private static readonly Vector128<short> Mask_UInt16_0x3C = Vector128.Create((short)0x3C);
private static readonly Vector128<short> Mask_UInt16_0x3E = Vector128.Create((short)0x3E);
private static readonly Vector128<short> Mask_UInt16_0x5C = Vector128.Create((short)0x5C);
private static readonly Vector128<short> Mask_UInt16_0x60 = Vector128.Create((short)0x60);

private static readonly Vector128<short> Mask_UInt16_0x7E = Vector128.Create((short)0x7E);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<short> CreateEscapingMask(Vector128<short> sourceValue)
{
    Vector128<short> mask = Sse2.CompareLessThan(sourceValue, Mask_UInt16_0x20); // Control characters

    mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x22)); // Quotation Mark "
    mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x26)); // Ampersand &
    mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x27)); // Apostrophe '
    mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x2B)); // Plus sign +

    mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x3C)); // Less Than Sign <
    mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x3E)); // Greater Than Sign >
    mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x5C)); // Reverse Solidus \
    mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x60)); // Grave Access `

    mask = Sse2.Or(mask, Sse2.CompareGreaterThan(sourceValue, Mask_UInt16_0x7E)); // Tilde ~, anything above the ASCII range

    return mask;
}

Alternative Approaches Considered

  1. Instrinsics (with fixed) + loop-unrolling at the end
  2. Intrinsics (without fixed)
  3. Intrinsics (without fixed) + loop-unrolling at the end
  4. Intrinsics (without fixed) using do-while + loop-unrolling at the end
  5. Intrinsics (with fixed) + custom loop-unrolling reading 4 chars into ulong, 2 chars into a uint
  6. Intrinsics (with fixed) with no "leftover" loop at the end, but rather back-fill what's being read (to always read 8 at a time)
  7. Intrinsics (with fixed) with initial jump table, and "leftover" loop
  8. Regular for loop but use two ulongs for the bit-map rather than an array.

Some of these were better for small strings (< 8 characters) but not for larger strings compared to the chosen approach while others were equivalent to the new approach for larger strings but worse for small strings.

Results of the Approach Chosen

See benchmark below (Test_EscapingBenchmark.cs)

BenchmarkDotNet=v0.11.5.1159-nightly, OS=Windows 10.0.18362
Intel Core i7-6700 CPU 3.40GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=5.0.100-alpha1-014834
  [Host]     : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT
  Job-MRCJIT : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT

PowerPlanMode=00000000-0000-0000-0000-000000000000  IterationTime=250.0000 ms  MaxIterationCount=20  
MinIterationCount=15  WarmupCount=1  
Method TestStringData Mean Error StdDev Median Min Max Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
NeedsEscapingCurrent (1, -1, r) 2.887 ns 0.0887 ns 0.0871 ns 2.895 ns 2.735 ns 3.088 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (1, -1, r) 4.380 ns 0.5280 ns 0.5868 ns 4.372 ns 3.605 ns 5.735 ns 1.47 0.16 - - - -
NeedsEscapingCurrent (1, 0, <) 3.368 ns 0.1285 ns 0.1480 ns 3.344 ns 3.168 ns 3.630 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (1, 0, <) 3.656 ns 0.1261 ns 0.1402 ns 3.662 ns 3.401 ns 3.867 ns 1.09 0.07 - - - -
NeedsEscapingCurrent (100,(...)kuwu) [111] 126.466 ns 3.5668 ns 3.6628 ns 125.681 ns 121.479 ns 137.199 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (100,(...)kuwu) [111] 59.504 ns 1.4698 ns 1.6926 ns 59.482 ns 57.077 ns 63.394 ns 0.47 0.02 - - - -
NeedsEscapingCurrent (15, (...)dabu) [25] 19.427 ns 0.6043 ns 0.6205 ns 19.432 ns 18.416 ns 20.446 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (15, (...)dabu) [25] 14.305 ns 0.4438 ns 0.4932 ns 14.303 ns 13.364 ns 15.273 ns 0.74 0.04 - - - -
NeedsEscapingCurrent (16, (...)sqfa) [26] 20.646 ns 1.1050 ns 1.2725 ns 20.052 ns 19.277 ns 24.293 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (16, (...)sqfa) [26] 11.916 ns 0.9764 ns 1.1244 ns 11.968 ns 10.337 ns 14.752 ns 0.58 0.06 - - - -
NeedsEscapingCurrent (17, (...)aabr) [27] 21.111 ns 0.4511 ns 0.4632 ns 20.984 ns 20.612 ns 22.301 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (17, (...)aabr) [27] 12.426 ns 0.5233 ns 0.6026 ns 12.311 ns 11.702 ns 13.732 ns 0.59 0.03 - - - -
NeedsEscapingCurrent (2, -1, dd) 4.022 ns 0.2340 ns 0.2694 ns 3.933 ns 3.696 ns 4.566 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (2, -1, dd) 4.525 ns 0.1035 ns 0.0968 ns 4.490 ns 4.413 ns 4.748 ns 1.15 0.08 - - - -
NeedsEscapingCurrent (32, (...)dfpn) [42] 37.854 ns 0.5715 ns 0.5346 ns 37.702 ns 37.313 ns 38.947 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (32, (...)dfpn) [42] 19.412 ns 0.3837 ns 0.3402 ns 19.313 ns 18.979 ns 20.131 ns 0.51 0.01 - - - -
NeedsEscapingCurrent (4, -1, negs) 5.834 ns 0.1179 ns 0.1103 ns 5.790 ns 5.728 ns 6.068 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (4, -1, negs) 6.614 ns 0.1549 ns 0.1522 ns 6.568 ns 6.389 ns 6.915 ns 1.13 0.04 - - - -
NeedsEscapingCurrent (7, -1, netggni) 9.243 ns 0.2037 ns 0.1906 ns 9.189 ns 9.037 ns 9.685 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (7, -1, netggni) 9.987 ns 0.2297 ns 0.2149 ns 9.886 ns 9.696 ns 10.506 ns 1.08 0.02 - - - -
NeedsEscapingCurrent (7, 0, <etggni) 3.307 ns 0.0980 ns 0.1089 ns 3.249 ns 3.176 ns 3.535 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (7, 0, <etggni) 3.868 ns 0.3175 ns 0.3656 ns 3.833 ns 3.289 ns 4.521 ns 1.17 0.10 - - - -
NeedsEscapingCurrent (7, 1, n<tggni) 4.882 ns 0.1164 ns 0.1032 ns 4.878 ns 4.744 ns 5.113 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (7, 1, n<tggni) 4.505 ns 0.0862 ns 0.0806 ns 4.475 ns 4.427 ns 4.664 ns 0.92 0.02 - - - -
NeedsEscapingCurrent (7, 2, ne<ggni) 4.978 ns 0.0946 ns 0.0885 ns 4.947 ns 4.845 ns 5.144 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (7, 2, ne<ggni) 5.800 ns 0.2560 ns 0.2740 ns 5.726 ns 5.447 ns 6.386 ns 1.17 0.06 - - - -
NeedsEscapingCurrent (7, 3, net<gni) 6.052 ns 0.1581 ns 0.1692 ns 6.031 ns 5.820 ns 6.428 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (7, 3, net<gni) 6.657 ns 0.1618 ns 0.1589 ns 6.631 ns 6.438 ns 6.920 ns 1.10 0.04 - - - -
NeedsEscapingCurrent (7, 4, netg<ni) 6.971 ns 0.1549 ns 0.1373 ns 6.939 ns 6.807 ns 7.330 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (7, 4, netg<ni) 7.805 ns 0.1655 ns 0.1548 ns 7.754 ns 7.595 ns 8.176 ns 1.12 0.03 - - - -
NeedsEscapingCurrent (7, 5, netgg<i) 8.219 ns 0.2058 ns 0.2114 ns 8.165 ns 7.969 ns 8.702 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (7, 5, netgg<i) 8.745 ns 0.1525 ns 0.1427 ns 8.703 ns 8.499 ns 8.979 ns 1.06 0.03 - - - -
NeedsEscapingCurrent (7, 6, netggn<) 9.215 ns 0.2257 ns 0.2318 ns 9.151 ns 8.966 ns 9.636 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (7, 6, netggn<) 10.053 ns 0.2330 ns 0.2288 ns 10.012 ns 9.657 ns 10.530 ns 1.09 0.04 - - - -
NeedsEscapingCurrent (8, -1, jgnavpkd) 10.427 ns 0.1893 ns 0.1580 ns 10.419 ns 10.183 ns 10.746 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (8, -1, jgnavpkd) 6.729 ns 0.1442 ns 0.1349 ns 6.698 ns 6.584 ns 7.071 ns 0.65 0.02 - - - -
NeedsEscapingCurrent (8, 0, <gnavpkd) 3.380 ns 0.0966 ns 0.1034 ns 3.405 ns 3.217 ns 3.569 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (8, 0, <gnavpkd) 6.481 ns 0.1822 ns 0.1872 ns 6.408 ns 6.274 ns 6.968 ns 1.92 0.05 - - - -
NeedsEscapingCurrent (8, 1, j<navpkd) 5.033 ns 0.1338 ns 0.1252 ns 5.008 ns 4.832 ns 5.219 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (8, 1, j<navpkd) 6.420 ns 0.1383 ns 0.1226 ns 6.424 ns 6.263 ns 6.686 ns 1.28 0.04 - - - -
NeedsEscapingCurrent (8, 2, jg<avpkd) 4.972 ns 0.1150 ns 0.1076 ns 4.921 ns 4.841 ns 5.175 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (8, 2, jg<avpkd) 6.449 ns 0.1261 ns 0.1179 ns 6.419 ns 6.286 ns 6.668 ns 1.30 0.03 - - - -
NeedsEscapingCurrent (8, 3, jgn<vpkd) 6.123 ns 0.2052 ns 0.2363 ns 6.057 ns 5.678 ns 6.720 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (8, 3, jgn<vpkd) 7.560 ns 1.0952 ns 1.2612 ns 6.708 ns 6.291 ns 9.128 ns 1.24 0.21 - - - -
NeedsEscapingCurrent (8, 4, jgna<pkd) 7.049 ns 0.1617 ns 0.1433 ns 7.085 ns 6.793 ns 7.284 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (8, 4, jgna<pkd) 8.215 ns 1.1845 ns 1.3640 ns 8.984 ns 6.329 ns 9.701 ns 1.11 0.21 - - - -
NeedsEscapingCurrent (8, 5, jgnav<kd) 8.175 ns 0.1894 ns 0.2026 ns 8.096 ns 7.969 ns 8.548 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (8, 5, jgnav<kd) 6.392 ns 0.0998 ns 0.0934 ns 6.365 ns 6.262 ns 6.604 ns 0.78 0.02 - - - -
NeedsEscapingCurrent (8, 6, jgnavp<d) 9.417 ns 0.2312 ns 0.2663 ns 9.365 ns 9.033 ns 10.058 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (8, 6, jgnavp<d) 6.606 ns 0.1920 ns 0.2134 ns 6.680 ns 6.306 ns 6.967 ns 0.70 0.04 - - - -
NeedsEscapingCurrent (8, 7, jgnavpk<) 10.452 ns 0.2368 ns 0.2632 ns 10.418 ns 10.090 ns 11.049 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (8, 7, jgnavpk<) 6.669 ns 0.2125 ns 0.2447 ns 6.730 ns 6.332 ns 7.128 ns 0.64 0.03 - - - -
NeedsEscapingCurrent (9, -1, csvobsdxs) 11.689 ns 0.2281 ns 0.2134 ns 11.571 ns 11.499 ns 12.069 ns 1.00 0.00 - - - -
NeedsEscapingIntrinsics (9, -1, csvobsdxs) 7.015 ns 0.1766 ns 0.1652 ns 6.943 ns 6.828 ns 7.456 ns 0.60 0.02 - - - -

https://github.com/dotnet/performance/blob/c468b2967a74a58d3b351a61b35457362d6f078e/src/benchmarks/micro/corefx/System.Text.Json/Utf8JsonWriter/Perf.Basic.cs#L85-L116

Before:

Method Formatted SkipValidation DataSize Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
WriteBasicUtf16 False True 10 931.2 ns 21.15 ns 24.35 ns 923.8 ns 899.5 ns 991.1 ns 0.0253 - - 120 B

After:

Method Formatted SkipValidation DataSize Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
WriteBasicUtf16 False True 10 917.5 ns 18.47 ns 17.28 ns 913.5 ns 895.9 ns 955.9 ns 0.0254 - - 120 B

See benchmark below (Test_EscapingWriter.cs)

Before:

Method DataLength NegativeIndex Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
NeedsEscapingCurrent 32 -1 73.81 us 3.839 us 4.421 us 73.71 us 66.18 us 82.50 us - - - -
NeedsEscapingCurrent 32 12 135.92 us 6.858 us 7.623 us 133.45 us 126.87 us 155.36 us - - - 2 B

After:

Method DataLength NegativeIndex Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
NeedsEscapingCurrent 32 -1 50.23 us 0.513 us 0.455 us 50.07 us 49.45 us 50.91 us - - - -
NeedsEscapingCurrent 32 12 133.23 us 2.613 us 2.566 us 132.28 us 130.12 us 137.33 us - - - 1 B

https://github.com/dotnet/performance/blob/c468b2967a74a58d3b351a61b35457362d6f078e/src/benchmarks/micro/corefx/System.Text.Json/Serializer/WriteJson.cs#L16-L19

Before:

LoginViewModel

Method Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
SerializeToString 631.8 ns 57.98 ns 66.77 ns 614.1 ns 552.8 ns 784.0 ns 0.0805 - - 344 B

Location

Method Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
SerializeToString 1.363 us 0.0820 us 0.0945 us 1.373 us 1.175 us 1.519 us 0.1321 - - 584 B

IndexViewModel

Method Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
SerializeToString 35.63 us 0.975 us 1.044 us 35.28 us 34.23 us 37.97 us 6.0153 - - 25.01 KB

MyEventsListerViewModel

Method Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
SerializeToString 704.1 us 22.02 us 25.35 us 694.0 us 671.9 us 758.0 us 91.3462 43.2692 43.2692 386.95 KB

After:

LoginViewModel

Method Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
SerializeToString 510.6 ns 11.75 ns 13.53 ns 511.4 ns 488.5 ns 537.3 ns 0.0812 - - 344 B

Location

Method Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
SerializeToString 1.241 us 0.0517 us 0.0596 us 1.232 us 1.164 us 1.403 us 0.1380 - - 584 B

IndexViewModel

Method Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
SerializeToString 28.38 us 0.577 us 0.664 us 28.36 us 27.51 us 29.87 us 5.9947 - - 24.97 KB

MyEventsListerViewModel

Method Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
SerializeToString 672.3 us 19.72 us 18.45 us 666.0 us 653.3 us 719.3 us 91.3462 43.2692 43.2692 386.88 KB

See benchmark below (Test_SerializingNuget.cs)

Before:

Method Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
SerializeToString_Original_STJson 791.7 ms 15.69 ms 16.12 ms 787.4 ms 772.0 ms 827.1 ms - - - 787.3 MB

After:

Method Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
SerializeToString_Original_STJson 698.4 ms 6.63 ms 5.53 ms 699.5 ms 690.9 ms 708.4 ms - - - 787.3 MB

Conclusions:

Here's a summary of the results when the user doesn't customize the encoder via the JsonSerializerOptions or JsonWriterOptions (i.e. uses the default encoder behavior).

  1. For end-to-end scenario (such as serializing commonly found objects/payloads), there is a 10-20% improvement.
  2. Writing relatively large JSON strings using the writer got ~30% faster (i.e. greater than 16 characters).
  3. Checking for escaping strings that are less than 8 characters is ~20-50% slower, but larger strings (i.e. greater than 16 character) got 2-3x faster.
  4. If a character is found that needs escaping within the first 8 characters, there is a 20-90% regression. Otherwise, there is a a 2-3x performance improvement depending on where the first character that needs escaping is found (say index greater than 16).

Next Steps:

  1. Add similar support and tests for UTF-8 bytes, not just UTF-16 characters.
  2. Evaluate if the trade-off is worth it for property names which tend to be small (2-8 characters), compared to values.
  3. Consider optimizing the commonly used built-in JavascriptEncoder statics using similar techniques.
  4. Apply non-Sse2 based optimizations where Sse2 isn't supported rather than processing one character at a time.
  5. Rather than returning the first index to escape, return the whole mask and escape all characters that need to be escaped at once (within the block of 8) and return back to the "fast" non-escaping path, rather than writing one character at a time whenever a single character is found that needs escaping.
public unsafe class Test_Escaping
{
private JavaScriptEncoder _encoder;
[ParamsSource(nameof(JsonStringData))]
public (int Length, int EscapeCharIndex, string JsonString) TestStringData { get; set; }
private static readonly int[] dataLengths = new int[] { 1, 2, 4, 7, 8, 9, 15, 16, 17, 32, 100 };
private static readonly int[] subsetToAddEscapedCharacters = new int[] { 1, 7, 8 };
public static IEnumerable<(int, int, string)> JsonStringData()
{
var random = new Random(42);
for (int j = 0; j < dataLengths.Length; j++)
{
int dataLength = dataLengths[j];
var array = new char[dataLength];
for (int i = 0; i < dataLength; i++)
{
array[i] = (char)random.Next(97, 123);
}
yield return (dataLength, -1, new string(array)); // No character requires escaping
if (subsetToAddEscapedCharacters.Contains(dataLength))
{
for (int i = 0; i < dataLength; i++)
{
char currentChar = array[i];
array[i] = '<';
yield return (dataLength, i, new string(array)); // One character requires escaping at index i
array[i] = currentChar;
}
}
}
}
[GlobalSetup]
public void Setup()
{
_encoder = null;
}
[BenchmarkCategory(Categories.CoreFX, Categories.JSON)]
[Benchmark(Baseline = true)]
public int NeedsEscapingCurrent()
{
return NeedsEscaping(TestStringData.JsonString, _encoder);
}
[BenchmarkCategory(Categories.CoreFX, Categories.JSON)]
[Benchmark]
public int NeedsEscapingIntrinsics()
{
return NeedsEscapingIntrinsics(TestStringData.JsonString, _encoder);
}
public static unsafe int NeedsEscaping(ReadOnlySpan<char> value, JavaScriptEncoder encoder)
{
int idx;
// Some implementations of JavascriptEncoder.FindFirstCharacterToEncode may not accept
// null pointers and gaurd against that. Hence, check up-front and fall down to return -1.
if (encoder != null && !value.IsEmpty)
{
fixed (char* ptr = value)
{
idx = encoder.FindFirstCharacterToEncode(ptr, value.Length);
}
goto Return;
}
for (idx = 0; idx < value.Length; idx++)
{
if (NeedsEscaping(value[idx]))
{
goto Return;
}
}
idx = -1; // all characters allowed
Return:
return idx;
}
public static int NeedsEscapingIntrinsics(ReadOnlySpan<char> value, JavaScriptEncoder encoder)
{
fixed (char* ptr = value)
{
int idx = 0;
// Some implementations of JavascriptEncoder.FindFirstCharacterToEncode may not accept
// null pointers and gaurd against that. Hence, check up-front and fall down to return -1.
if (encoder != null && !value.IsEmpty)
{
idx = encoder.FindFirstCharacterToEncode(ptr, value.Length);
goto Return;
}
short* startingAddress = (short*)ptr;
while (value.Length - 8 >= idx)
{
Vector128<short> sourceValue = Sse2.LoadVector128(startingAddress);
Vector128<short> mask = CreateEscapingMask(sourceValue);
int index = Sse2.MoveMask(mask.AsByte());
// TrailingZeroCount is relatively expensive, avoid it if possible.
if (index != 0)
{
idx += BitOperations.TrailingZeroCount(index) >> 1;
goto Return;
}
idx += 8;
startingAddress += 8;
}
for (; idx < value.Length; idx++)
{
if (NeedsEscaping(*(ptr + idx)))
{
goto Return;
}
}
idx = -1; // all characters allowed
Return:
return idx;
}
}
public const int LastAsciiCharacter = 0x7F;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool NeedsEscaping(char value) => value > LastAsciiCharacter || AllowList[value] == 0;
private static ReadOnlySpan<byte> AllowList => new byte[byte.MaxValue + 1]
{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // U+0000..U+000F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // U+0010..U+001F
1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, // U+0020..U+002F
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, // U+0030..U+003F
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // U+0040..U+004F
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // U+0050..U+005F
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // U+0060..U+006F
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // U+0070..U+007F
// Also include the ranges from U+0080 to U+00FF for performance to avoid UTF8 code from checking boundary.
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // U+00F0..U+00FF
};
private static readonly Vector128<short> Mask_UInt16_0x20 = Vector128.Create((short)0x20);
private static readonly Vector128<short> Mask_UInt16_0x22 = Vector128.Create((short)0x22);
private static readonly Vector128<short> Mask_UInt16_0x26 = Vector128.Create((short)0x26);
private static readonly Vector128<short> Mask_UInt16_0x27 = Vector128.Create((short)0x27);
private static readonly Vector128<short> Mask_UInt16_0x2B = Vector128.Create((short)0x2B);
private static readonly Vector128<short> Mask_UInt16_0x3C = Vector128.Create((short)0x3C);
private static readonly Vector128<short> Mask_UInt16_0x3E = Vector128.Create((short)0x3E);
private static readonly Vector128<short> Mask_UInt16_0x5C = Vector128.Create((short)0x5C);
private static readonly Vector128<short> Mask_UInt16_0x60 = Vector128.Create((short)0x60);
private static readonly Vector128<short> Mask_UInt16_0x7E = Vector128.Create((short)0x7E);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<short> CreateEscapingMask(Vector128<short> sourceValue)
{
Vector128<short> mask = Sse2.CompareLessThan(sourceValue, Mask_UInt16_0x20); // Control characters
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x22)); // Quotation Mark "
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x26)); // Ampersand &
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x27)); // Apostrophe '
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x2B)); // Plus sign +
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x3C)); // Less Than Sign <
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x3E)); // Greater Than Sign >
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x5C)); // Reverse Solidus \
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x60)); // Grave Access `
mask = Sse2.Or(mask, Sse2.CompareGreaterThan(sourceValue, Mask_UInt16_0x7E)); // Tilde ~, anything above the ASCII range
return mask;
}
}
public unsafe class TestEscapingWriter
{
private string _source;
private byte[] _sourceUtf8;
private Utf8JsonWriter _writer;
private ArrayBufferWriter<byte> _output;
//[Params(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 16, 17, 31, 32, 33, 47, 63, 64, 65, 100, 1000)]
//[Params(1, 2, 4, 10, 15, 16, 17, 31, 32, 33, 47, 64, 100, 1000)]
[Params(32)]
public int DataLength { get; set; }
[Params(-1, 12)]
public int NegativeIndex { get; set; }
[GlobalSetup]
public void Setup()
{
var random = new Random(42);
var array = new char[DataLength];
for (int i = 0; i < DataLength; i++)
{
array[i] = (char)random.Next(97, 123);
}
if (NegativeIndex != -1)
{
array[NegativeIndex] = '<'; // '¢'
}
_source = new string(array);
_sourceUtf8 = Encoding.UTF8.GetBytes(_source);
_output = new ArrayBufferWriter<byte>();
_writer = new Utf8JsonWriter(_output, new JsonWriterOptions { SkipValidation = true });
}
[BenchmarkCategory(Categories.CoreFX, Categories.JSON)]
[Benchmark]
public void NeedsEscapingCurrent()
{
_output.Clear();
for (int i = 0; i < 1_000; i++)
_writer.WriteStringValue(_source);
}
}
public class Test_SerializingNuget
{
private string _serialized;
private SearchResults _original;
[GlobalSetup]
public void Setup()
{
_serialized = File.ReadAllText(@"E:\GitHub\Fork\Benchmarks\TestData\JsonParsingBenchmark\dotnet-core.json");
_original = JsonSerializer.Deserialize<SearchResults>(_serialized);
}
[BenchmarkCategory(Categories.CoreFX, Categories.JSON)]
[Benchmark]
public string SerializeToString_Original_STJson()
{
return JsonSerializer.Serialize<SearchResults>(_original);
}
public class SearchResult
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("version")]
public string Version { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("versions")]
public List<SearchResultVersion> Versions { get; set; }
[JsonPropertyName("authors")]
public List<string> Authors { get; set; }
[JsonPropertyName("iconUrl")]
public string IconUrl { get; set; }
[JsonPropertyName("licenseUrl")]
public string LicenseUrl { get; set; }
[JsonPropertyName("owners")]
public List<string> Owners { get; set; }
[JsonPropertyName("projectUrl")]
public string ProjectUrl { get; set; }
[JsonPropertyName("registration")]
public string Registration { get; set; }
[JsonPropertyName("summary")]
public string Summary { get; set; }
[JsonPropertyName("tags")]
public List<string> Tags { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("totalDownloads")]
public int TotalDownloads { get; set; }
[JsonPropertyName("verified")]
public bool Verified { get; set; }
}
public class SearchResults
{
[JsonPropertyName("totalHits")]
public int TotalHits { get; set; }
[JsonPropertyName("data")]
public List<SearchResult> Data { get; set; }
}
public class SearchResultVersion
{
[JsonPropertyName("@id")]
public string Id { get; set; }
[JsonPropertyName("version")]
public string Version { get; set; }
[JsonPropertyName("downloads")]
public int Downloads { get; set; }
}
}
// 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.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using System.Text.Encodings.Web;
using BenchmarkDotNet.Attributes;
using MicroBenchmarks;
namespace System.Text.Json.Tests
{
public unsafe class Test_EscapingComparison
{
private string _source;
private byte[] _sourceUtf8;
private byte[] _sourceNegativeUtf8;
private JavaScriptEncoder _encoder;
[Params(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 16, 17, 31, 32, 33, 47, 63, 64, 65, 100, 1000)]
public int DataLength { get; set; }
[Params(-1)]
public int NegativeIndex { get; set; }
[GlobalSetup]
public void Setup()
{
var random = new Random(42);
var array = new char[DataLength];
for (int i = 0; i < DataLength; i++)
{
array[i] = (char)random.Next(97, 123);
}
_source = new string(array);
_sourceUtf8 = Encoding.UTF8.GetBytes(_source);
if (NegativeIndex != -1)
{
array[NegativeIndex] = '<';
_source = new string(array);
}
_sourceNegativeUtf8 = Encoding.UTF8.GetBytes(_source);
_encoder = null;
}
[BenchmarkCategory(Categories.CoreFX, Categories.JSON)]
[Benchmark(Baseline = true)]
public int NeedsEscapingCurrent()
{
return NeedsEscaping(_source, _encoder);
}
[BenchmarkCategory(Categories.CoreFX, Categories.JSON)]
[Benchmark]
public int NeedsEscapingNewFixed()
{
return NeedsEscaping_New_Fixed(_source, _encoder);
}
[BenchmarkCategory(Categories.CoreFX, Categories.JSON)]
[Benchmark]
public int NeedsEscapingNewFixedLoopUnrolled()
{
return NeedsEscaping_New_Fixed_LoopUnrolled(_source, _encoder);
}
[BenchmarkCategory(Categories.CoreFX, Categories.JSON)]
[Benchmark]
public int NeedsEscapingNewLoopUnrolled()
{
return NeedsEscaping_New_LoopUnrolled(_source, _encoder);
}
[BenchmarkCategory(Categories.CoreFX, Categories.JSON)]
[Benchmark]
public int NeedsEscapingNewDoWhile()
{
return NeedsEscaping_New_DoWhile(_source, _encoder);
}
[BenchmarkCategory(Categories.CoreFX, Categories.JSON)]
[Benchmark]
public int NeedsEscapingBackFill()
{
return NeedsEscaping_BackFill(_source, _encoder);
}
[BenchmarkCategory(Categories.CoreFX, Categories.JSON)]
[Benchmark]
public int NeedsEscapingJumpTable()
{
return NeedsEscaping_JumpTable(_source, _encoder);
}
private static ReadOnlySpan<byte> AllowList => new byte[byte.MaxValue + 1]
{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // U+0000..U+000F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // U+0010..U+001F
1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, // U+0020..U+002F
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, // U+0030..U+003F
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // U+0040..U+004F
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // U+0050..U+005F
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // U+0060..U+006F
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // U+0070..U+007F
// Also include the ranges from U+0080 to U+00FF for performance to avoid UTF8 code from checking boundary.
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // U+00F0..U+00FF
};
private static bool NeedsEscaping(byte value) => AllowList[value] == 0;
public static int NeedsEscaping(ReadOnlySpan<byte> value, JavaScriptEncoder encoder)
{
int idx;
if (encoder != null)
{
idx = encoder.FindFirstCharacterToEncodeUtf8(value);
goto Return;
}
for (idx = 0; idx < value.Length; idx++)
{
if (NeedsEscaping(value[idx]))
{
goto Return;
}
}
idx = -1; // all characters allowed
Return:
return idx;
}
public static unsafe int NeedsEscaping(ReadOnlySpan<char> value, JavaScriptEncoder encoder)
{
int idx;
// Some implementations of JavascriptEncoder.FindFirstCharacterToEncode may not accept
// null pointers and gaurd against that. Hence, check up-front and fall down to return -1.
if (encoder != null && !value.IsEmpty)
{
fixed (char* ptr = value)
{
idx = encoder.FindFirstCharacterToEncode(ptr, value.Length);
}
goto Return;
}
for (idx = 0; idx < value.Length; idx++)
{
if (NeedsEscaping(value[idx]))
{
goto Return;
}
}
idx = -1; // all characters allowed
Return:
return idx;
}
public const int LastAsciiCharacter = 0x7F;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool NeedsEscaping(char value) => value > LastAsciiCharacter || AllowList[value] == 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool NeedsEscaping_NoBoundsCheck(char value) => AllowList[value] == 0;
public static readonly ushort[] TrailingAlignmentMask = new ushort[64]
{
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF,
0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
0x0000, 0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
0x0000, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
};
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<short> CreateEscapingMask(Vector128<short> sourceValue)
{
Vector128<short> mask = Sse2.CompareLessThan(sourceValue, Mask_UInt16_0x20); // Control characters
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x22)); // Quotation Mark "
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x26)); // Ampersand &
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x27)); // Apostrophe '
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x2B)); // Plus sign +
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x3C)); // Less Than Sign <
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x3E)); // Greater Than Sign >
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x5C)); // Reverse Solidus \
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x60)); // Grave Access `
mask = Sse2.Or(mask, Sse2.CompareGreaterThan(sourceValue, Mask_UInt16_0x7E)); // Tilde ~, anything above the ASCII range
return mask;
}
private static unsafe int BackFillReadAndProcess(short* startingAddress, short* trailingMaskIndex)
{
Vector128<short> sourceValueWithBackFill = Sse2.LoadVector128(startingAddress);
Vector128<short> trailingMask = Sse2.LoadVector128(trailingMaskIndex);
Vector128<short> sourceValue = Sse2.And(sourceValueWithBackFill, trailingMask);
Vector128<short> mask = CreateEscapingMask(sourceValue);
mask = Sse2.And(mask, trailingMask);
return Sse2.MoveMask(Vector128.AsByte(mask));
}
public static unsafe int NeedsEscaping_JumpTable(ReadOnlySpan<char> value, JavaScriptEncoder encoder)
{
fixed (char* ptr = value)
{
int idx;
// Some implementations of JavascriptEncoder.FindFirstCharacterToEncode may not accept
// null pointers and gaurd against that. Hence, check up-front and fall down to return -1.
if (encoder != null && !value.IsEmpty)
{
idx = encoder.FindFirstCharacterToEncode(ptr, value.Length);
goto Return;
}
idx = -1;
switch (value.Length)
{
case 7:
if (NeedsEscaping(*(ptr + 6))) idx = 6;
goto case 6;
case 6:
if (NeedsEscaping(*(ptr + 5))) idx = 5;
goto case 5;
case 5:
if (NeedsEscaping(*(ptr + 4))) idx = 4;
goto case 4;
case 4:
if (NeedsEscaping(*(ptr + 3))) idx = 3;
goto case 3;
case 3:
if (NeedsEscaping(*(ptr + 2))) idx = 2;
goto case 2;
case 2:
if (NeedsEscaping(*(ptr + 1))) idx = 1;
goto case 1;
case 1:
if (NeedsEscaping(*(ptr + 0))) idx = 0;
break;
case 0:
break;
default:
{
short* startingAddress = (short*)ptr;
idx = 0;
while (value.Length - 8 >= idx)
{
Vector128<short> sourceValue = Sse2.LoadVector128(startingAddress);
Vector128<short> mask = CreateEscapingMask(sourceValue);
int index = Sse2.MoveMask(mask.AsByte());
// TrailingZeroCount is relatively expensive, avoid it if possible.
if (index != 0)
{
idx += BitOperations.TrailingZeroCount(index) >> 1;
goto Return;
}
idx += 8;
startingAddress += 8;
}
int remainder = value.Length - idx;
if (remainder > 0)
{
for (; idx < value.Length; idx++)
{
if (NeedsEscaping(value[idx]))
{
goto Return;
}
}
}
idx = -1;
break;
}
}
Return:
return idx;
}
}
public static unsafe int NeedsEscaping_BackFill(ReadOnlySpan<char> value, JavaScriptEncoder encoder)
{
fixed (ushort* pTrailingAlignmentMask = &TrailingAlignmentMask[0])
fixed (char* ptr = value)
{
int idx;
// Some implementations of JavascriptEncoder.FindFirstCharacterToEncode may not accept
// null pointers and gaurd against that. Hence, check up-front and fall down to return -1.
if (value.IsEmpty)
{
idx = -1;
goto Return;
}
if (encoder != null)
{
idx = encoder.FindFirstCharacterToEncode(ptr, value.Length);
goto Return;
}
short* startingAddress = (short*)ptr;
if (value.Length < 8)
{
int backFillCount = 8 - value.Length;
int index = BackFillReadAndProcess(startingAddress - backFillCount, (short*)pTrailingAlignmentMask + (value.Length * 8));
// TrailingZeroCount is relatively expensive, avoid it if possible.
if (index != 0)
{
idx = (BitOperations.TrailingZeroCount(index) >> 1) - backFillCount;
goto Return;
}
}
else
{
idx = 0;
while (value.Length - 8 >= idx)
{
Vector128<short> sourceValue = Sse2.LoadVector128(startingAddress);
Vector128<short> mask = CreateEscapingMask(sourceValue);
int index = Sse2.MoveMask(mask.AsByte());
// TrailingZeroCount is relatively expensive, avoid it if possible.
if (index != 0)
{
idx += BitOperations.TrailingZeroCount(index) >> 1;
goto Return;
}
idx += 8;
startingAddress += 8;
}
int remainder = value.Length - idx;
if (remainder > 0)
{
int backFillCount = 8 - remainder;
int index = BackFillReadAndProcess(startingAddress - backFillCount, (short*)pTrailingAlignmentMask + (remainder * 8));
// TrailingZeroCount is relatively expensive, avoid it if possible.
if (index != 0)
{
idx += (BitOperations.TrailingZeroCount(index) >> 1) - backFillCount;
goto Return;
}
}
}
idx = -1; // all characters allowed
Return:
return idx;
}
}
public static int NeedsEscaping_New_Fixed(ReadOnlySpan<char> value, JavaScriptEncoder encoder)
{
fixed (char* ptr = value)
{
int idx = 0;
// Some implementations of JavascriptEncoder.FindFirstCharacterToEncode may not accept
// null pointers and gaurd against that. Hence, check up-front and fall down to return -1.
if (encoder != null && !value.IsEmpty)
{
idx = encoder.FindFirstCharacterToEncode(ptr, value.Length);
goto Return;
}
short* startingAddress = (short*)ptr;
while (value.Length - 8 >= idx)
{
Vector128<short> sourceValue = Sse2.LoadVector128(startingAddress);
Vector128<short> mask = CreateEscapingMask(sourceValue);
int index = Sse2.MoveMask(mask.AsByte());
// TrailingZeroCount is relatively expensive, avoid it if possible.
if (index != 0)
{
idx += BitOperations.TrailingZeroCount(index) >> 1;
goto Return;
}
idx += 8;
startingAddress += 8;
}
for (; idx < value.Length; idx++)
{
if (NeedsEscaping(*(ptr + idx)))
{
goto Return;
}
}
idx = -1; // all characters allowed
Return:
return idx;
}
}
public static int NeedsEscaping_New_Fixed_LoopUnrolled(ReadOnlySpan<char> value, JavaScriptEncoder encoder)
{
fixed (char* ptr = value)
{
int idx = 0;
// Some implementations of JavascriptEncoder.FindFirstCharacterToEncode may not accept
// null pointers and gaurd against that. Hence, check up-front and fall down to return -1.
if (encoder != null && !value.IsEmpty)
{
idx = encoder.FindFirstCharacterToEncode(ptr, value.Length);
goto Return;
}
short* startingAddress = (short*)ptr;
while (value.Length - 8 >= idx)
{
Vector128<short> sourceValue = Sse2.LoadVector128(startingAddress);
Vector128<short> mask = CreateEscapingMask(sourceValue);
int index = Sse2.MoveMask(mask.AsByte());
// TrailingZeroCount is relatively expensive, avoid it if possible.
if (index != 0)
{
idx += BitOperations.TrailingZeroCount(index) >> 1;
goto Return;
}
idx += 8;
startingAddress += 8;
}
if (value.Length - 4 >= idx)
{
char* currentIndex = ptr + idx;
ulong candidateUInt64 = Unsafe.ReadUnaligned<ulong>(currentIndex);
if (AllCharsInUInt64AreAscii(candidateUInt64))
{
if (NeedsEscaping_NoBoundsCheck(*(currentIndex)) || NeedsEscaping_NoBoundsCheck(*(ptr + ++idx)) || NeedsEscaping_NoBoundsCheck(*(ptr + ++idx)) || NeedsEscaping_NoBoundsCheck(*(ptr + ++idx)))
{
goto Return;
}
idx++;
}
else
{
if (*(currentIndex) > 0x7F)
{
goto Return;
}
idx++;
if (*(ptr + idx) > 0x7F)
{
goto Return;
}
idx++;
if (*(ptr + idx) > 0x7F)
{
goto Return;
}
idx++;
goto Return;
}
}
if (value.Length - 2 >= idx)
{
char* currentIndex = ptr + idx;
uint candidateUInt32 = Unsafe.ReadUnaligned<uint>(currentIndex);
if (AllCharsInUInt32AreAscii(candidateUInt32))
{
if (NeedsEscaping_NoBoundsCheck(*(currentIndex)) || NeedsEscaping_NoBoundsCheck(*(ptr + ++idx)))
{
goto Return;
}
idx++;
}
else
{
if (*(currentIndex) > 0x7F)
{
goto Return;
}
idx++;
goto Return;
}
}
if (value.Length > idx)
{
if (NeedsEscaping(*(ptr + idx)))
{
goto Return;
}
}
idx = -1; // all characters allowed
Return:
return idx;
}
}
/// <summary>
/// Returns <see langword="true"/> iff all chars in <paramref name="value"/> are ASCII.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool AllCharsInUInt64AreAscii(ulong value)
{
return (value & ~0x007F007F_007F007Ful) == 0;
}
/// <summary>
/// Returns <see langword="true"/> iff all chars in <paramref name="value"/> are ASCII.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool AllCharsInUInt32AreAscii(uint value)
{
return (value & ~0x007F007Fu) == 0;
}
public static int NeedsEscaping_New_LoopUnrolled(ReadOnlySpan<char> value, JavaScriptEncoder encoder)
{
int idx = 0;
// Some implementations of JavascriptEncoder.FindFirstCharacterToEncode may not accept
// null pointers and gaurd against that. Hence, check up-front and fall down to return -1.
if (encoder != null && !value.IsEmpty)
{
fixed (char* ptr = value)
{
idx = encoder.FindFirstCharacterToEncode(ptr, value.Length);
}
goto Return;
}
ReadOnlySpan<byte> bytes = MemoryMarshal.AsBytes(value);
while (value.Length - 8 >= idx)
{
Vector128<short> sourceValue = MemoryMarshal.Read<Vector128<short>>(bytes.Slice(idx << 1));
Vector128<short> mask = CreateEscapingMask(sourceValue);
int index = Sse2.MoveMask(Vector128.AsByte(mask));
// TrailingZeroCount is relatively expensive, avoid it if possible.
if (index != 0)
{
idx += BitOperations.TrailingZeroCount(index) >> 1;
goto Return;
}
idx += 8;
}
if (value.Length - 4 >= idx)
{
if (NeedsEscaping(value[idx++]) || NeedsEscaping(value[idx++]) || NeedsEscaping(value[idx++]) || NeedsEscaping(value[idx++]))
{
idx--;
goto Return;
}
}
if (value.Length - 2 >= idx)
{
if (NeedsEscaping(value[idx++]) || NeedsEscaping(value[idx++]))
{
idx--;
goto Return;
}
}
if (value.Length > idx)
{
if (NeedsEscaping(value[idx]))
{
goto Return;
}
}
idx = -1; // all characters allowed
Return:
return idx;
}
public static int NeedsEscaping_New_DoWhile(ReadOnlySpan<char> value, JavaScriptEncoder encoder)
{
int idx = 0;
// Some implementations of JavascriptEncoder.FindFirstCharacterToEncode may not accept
// null pointers and gaurd against that. Hence, check up-front and fall down to return -1.
if (encoder != null && !value.IsEmpty)
{
fixed (char* ptr = value)
{
idx = encoder.FindFirstCharacterToEncode(ptr, value.Length);
}
goto Return;
}
if (value.Length - 8 >= idx)
{
ReadOnlySpan<byte> bytes = MemoryMarshal.AsBytes(value);
do
{
Vector128<short> sourceValue = MemoryMarshal.Read<Vector128<short>>(bytes.Slice(idx << 1));
Vector128<short> mask = CreateEscapingMask(sourceValue);
int index = Sse2.MoveMask(mask.AsByte());
// TrailingZeroCount is relatively expensive, avoid it if possible.
if (index != 0)
{
idx += BitOperations.TrailingZeroCount(index) >> 1;
goto Return;
}
idx += 8;
} while (value.Length - 8 >= idx);
}
if (value.Length - 4 >= idx)
{
if (NeedsEscaping(value[idx]) || NeedsEscaping(value[idx + 1]) || NeedsEscaping(value[idx + 2]) || NeedsEscaping(value[idx + 3]))
{
goto Return;
}
idx += 4;
}
if (value.Length - 2 >= idx)
{
if (NeedsEscaping(value[idx]) || NeedsEscaping(value[idx + 1]))
{
goto Return;
}
idx += 2;
}
if (value.Length > idx)
{
if (NeedsEscaping(value[idx]))
{
goto Return;
}
}
idx = -1; // all characters allowed
Return:
return idx;
}
public static int NeedsEscaping_New(ReadOnlySpan<char> value, JavaScriptEncoder encoder)
{
int idx = 0;
// Some implementations of JavascriptEncoder.FindFirstCharacterToEncode may not accept
// null pointers and gaurd against that. Hence, check up-front and fall down to return -1.
if (encoder != null && !value.IsEmpty)
{
fixed (char* ptr = value)
{
idx = encoder.FindFirstCharacterToEncode(ptr, value.Length);
}
goto Return;
}
ReadOnlySpan<byte> bytes = MemoryMarshal.AsBytes(value);
while (value.Length - 8 >= idx)
{
Vector128<short> sourceValue = MemoryMarshal.Read<Vector128<short>>(bytes.Slice(idx << 1));
Vector128<short> mask = Sse2.CompareLessThan(sourceValue, Mask_UInt16_0x20); // Control characters
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x22)); // Quotation Mark "
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x26)); // Ampersand &
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x27)); // Apostrophe '
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x2B)); // Plus sign +
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x3C)); // Less Than Sign <
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x3E)); // Greater Than Sign >
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x5C)); // Reverse Solidus \
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask_UInt16_0x60)); // Grave Access `
mask = Sse2.Or(mask, Sse2.CompareGreaterThan(sourceValue, Mask_UInt16_0x7E)); // Tilde ~, anything above the ASCII range
int index = Sse2.MoveMask(Vector128.AsByte(mask));
// TrailingZeroCount is relatively expensive, avoid it if possible.
if (index != 0)
{
idx += (BitOperations.TrailingZeroCount(index | 0xFFFF0000)) >> 1;
goto Return;
}
idx += 8;
}
for (; idx < value.Length; idx++)
{
if (NeedsEscaping(value[idx]))
{
goto Return;
}
}
idx = -1; // all characters allowed
Return:
return idx;
}
public static int NeedsEscaping_Encoding(ReadOnlySpan<byte> value, JavaScriptEncoder encoder)
{
return encoder.FindFirstCharacterToEncodeUtf8(value);
}
public static int NeedsEscaping_New(ReadOnlySpan<byte> value, JavaScriptEncoder encoder)
{
int idx = 0;
if (encoder != null)
{
idx = encoder.FindFirstCharacterToEncodeUtf8(value);
goto Return;
}
while (value.Length - 16 >= idx)
{
Vector128<sbyte> sourceValue = MemoryMarshal.Read<Vector128<sbyte>>(value.Slice(idx));
Vector128<sbyte> mask = Sse2.CompareLessThan(sourceValue, Mask0x20); // Control characters, and anything above 0x7E since sbyte.MaxValue is 0x7E
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask0x22)); // Quotation Mark "
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask0x26)); // Ampersand &
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask0x27)); // Apostrophe '
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask0x2B)); // Plus sign +
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask0x3C)); // Less Than Sign <
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask0x3E)); // Greater Than Sign >
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask0x5C)); // Reverse Solidus \
mask = Sse2.Or(mask, Sse2.CompareEqual(sourceValue, Mask0x60)); // Grave Access `
int index = Sse2.MoveMask(mask);
// TrailingZeroCount is relatively expensive, avoid it if possible.
if (index != 0)
{
idx += BitOperations.TrailingZeroCount(index | 0xFFFF0000);
goto Return;
}
idx += 16;
}
for (; idx < value.Length; idx++)
{
if (NeedsEscaping(value[idx]))
{
goto Return;
}
}
idx = -1; // all characters allowed
Return:
return idx;
}
private static readonly ulong mapLower = 17726150386525405184; // From BitConverter.ToUInt64(new byte[8] {0, 0, 0, 0, 220, 239, 255, 245}, 0);
private static readonly ulong mapUpper = 18374685929781592063; // From BitConverter.ToUInt64(new byte[8] {255, 255, 255, 247, 127, 255, 255, 254}, 0);
public static int NeedsEscaping_New_2(ReadOnlySpan<byte> value)
{
int idx = 0;
for (; idx < value.Length; idx++)
{
byte val = value[idx];
int whichFlag = val / 64;
switch (whichFlag)
{
case 0:
if ((mapLower & (ulong)(1 << val)) == 0)
goto Return;
break;
case 1:
if ((mapUpper & (ulong)(1 << (val - 64))) == 0)
goto Return;
break;
default:
goto Return;
}
}
idx = -1; // all characters allowed
Return:
return idx;
}
private static readonly Vector128<sbyte> Mask0x20 = Vector128.Create((sbyte)0x20);
private static readonly Vector128<sbyte> Mask0x22 = Vector128.Create((sbyte)0x22);
private static readonly Vector128<sbyte> Mask0x26 = Vector128.Create((sbyte)0x26);
private static readonly Vector128<sbyte> Mask0x27 = Vector128.Create((sbyte)0x27);
private static readonly Vector128<sbyte> Mask0x2B = Vector128.Create((sbyte)0x2B);
private static readonly Vector128<sbyte> Mask0x3C = Vector128.Create((sbyte)0x3C);
private static readonly Vector128<sbyte> Mask0x3E = Vector128.Create((sbyte)0x3E);
private static readonly Vector128<sbyte> Mask0x5C = Vector128.Create((sbyte)0x5C);
private static readonly Vector128<sbyte> Mask0x60 = Vector128.Create((sbyte)0x60);
private static readonly Vector128<sbyte> Mask0x7E = Vector128.Create((sbyte)0x7E);
private static readonly Vector128<short> Mask_UInt16_0x20 = Vector128.Create((short)0x20);
private static readonly Vector128<short> Mask_UInt16_0x22 = Vector128.Create((short)0x22);
private static readonly Vector128<short> Mask_UInt16_0x26 = Vector128.Create((short)0x26);
private static readonly Vector128<short> Mask_UInt16_0x27 = Vector128.Create((short)0x27);
private static readonly Vector128<short> Mask_UInt16_0x2B = Vector128.Create((short)0x2B);
private static readonly Vector128<short> Mask_UInt16_0x3C = Vector128.Create((short)0x3C);
private static readonly Vector128<short> Mask_UInt16_0x3E = Vector128.Create((short)0x3E);
private static readonly Vector128<short> Mask_UInt16_0x5C = Vector128.Create((short)0x5C);
private static readonly Vector128<short> Mask_UInt16_0x60 = Vector128.Create((short)0x60);
private static readonly Vector128<short> Mask_UInt16_0x7E = Vector128.Create((short)0x7E);
}
}
BenchmarkDotNet=v0.11.5.1159-nightly, OS=Windows 10.0.18362
Intel Core i7-6700 CPU 3.40GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=5.0.100-alpha1-014834
  [Host]     : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT
  Job-TFJCMY : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT

PowerPlanMode=00000000-0000-0000-0000-000000000000  IterationTime=250.0000 ms  MaxIterationCount=20  
MinIterationCount=15  WarmupCount=1  
Method DataLength NegativeIndex Mean Error StdDev Median Min Max Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
NeedsEscapingCurrent 1 -1 2.932 ns 0.3555 ns 0.3804 ns 2.807 ns 2.560 ns 3.845 ns 1.00 0.00 - - - -
NeedsEscapingNew 1 -1 4.067 ns 0.0990 ns 0.0878 ns 4.050 ns 3.940 ns 4.194 ns 1.45 0.13 - - - -
NeedsEscapingNewFixed 1 -1 5.549 ns 0.3232 ns 0.3722 ns 5.383 ns 5.040 ns 6.483 ns 1.93 0.27 - - - -
NeedsEscapingNewFixedLoopUnrolled 1 -1 5.428 ns 0.1346 ns 0.1259 ns 5.427 ns 5.256 ns 5.675 ns 1.94 0.20 - - - -
NeedsEscapingNewLoopUnrolled 1 -1 3.733 ns 0.0967 ns 0.0905 ns 3.724 ns 3.593 ns 3.880 ns 1.33 0.12 - - - -
NeedsEscapingNewDoWhile 1 -1 4.311 ns 0.0913 ns 0.0854 ns 4.345 ns 4.192 ns 4.430 ns 1.54 0.15 - - - -
NeedsEscapingBackFill 1 -1 10.198 ns 0.2415 ns 0.2480 ns 10.195 ns 9.820 ns 10.724 ns 3.57 0.38 - - - -
NeedsEscapingJumpTable 1 -1 5.504 ns 0.1185 ns 0.1050 ns 5.509 ns 5.323 ns 5.670 ns 1.96 0.18 - - - -
NeedsEscapingCurrent 2 -1 3.893 ns 0.1349 ns 0.1500 ns 3.917 ns 3.719 ns 4.232 ns 1.00 0.00 - - - -
NeedsEscapingNew 2 -1 5.005 ns 0.1666 ns 0.1918 ns 4.955 ns 4.729 ns 5.445 ns 1.29 0.06 - - - -
NeedsEscapingNewFixed 2 -1 5.990 ns 0.1862 ns 0.1742 ns 5.979 ns 5.799 ns 6.427 ns 1.55 0.09 - - - -
NeedsEscapingNewFixedLoopUnrolled 2 -1 6.262 ns 0.1336 ns 0.1250 ns 6.285 ns 6.095 ns 6.500 ns 1.61 0.07 - - - -
NeedsEscapingNewLoopUnrolled 2 -1 4.693 ns 0.1503 ns 0.1731 ns 4.636 ns 4.471 ns 5.003 ns 1.21 0.06 - - - -
NeedsEscapingNewDoWhile 2 -1 5.173 ns 0.1198 ns 0.1121 ns 5.175 ns 5.024 ns 5.423 ns 1.33 0.06 - - - -
NeedsEscapingBackFill 2 -1 10.848 ns 0.5205 ns 0.5994 ns 10.894 ns 9.796 ns 11.890 ns 2.78 0.14 - - - -
NeedsEscapingJumpTable 2 -1 5.637 ns 0.1908 ns 0.2042 ns 5.532 ns 5.405 ns 6.091 ns 1.45 0.08 - - - -
NeedsEscapingCurrent 3 -1 4.791 ns 0.1271 ns 0.1413 ns 4.718 ns 4.649 ns 5.110 ns 1.00 0.00 - - - -
NeedsEscapingNew 3 -1 5.692 ns 0.0988 ns 0.0876 ns 5.686 ns 5.507 ns 5.848 ns 1.19 0.04 - - - -
NeedsEscapingNewFixed 3 -1 6.741 ns 0.1563 ns 0.1385 ns 6.699 ns 6.573 ns 7.002 ns 1.41 0.06 - - - -
NeedsEscapingNewFixedLoopUnrolled 3 -1 7.132 ns 0.1769 ns 0.1966 ns 7.141 ns 6.870 ns 7.503 ns 1.49 0.05 - - - -
NeedsEscapingNewLoopUnrolled 3 -1 5.603 ns 0.1874 ns 0.2158 ns 5.617 ns 5.230 ns 5.953 ns 1.17 0.04 - - - -
NeedsEscapingNewDoWhile 3 -1 6.063 ns 0.1660 ns 0.1911 ns 6.099 ns 5.735 ns 6.324 ns 1.26 0.06 - - - -
NeedsEscapingBackFill 3 -1 10.175 ns 0.5950 ns 0.6110 ns 9.910 ns 9.533 ns 11.584 ns 2.12 0.11 - - - -
NeedsEscapingJumpTable 3 -1 6.271 ns 0.2174 ns 0.2033 ns 6.206 ns 6.017 ns 6.647 ns 1.31 0.06 - - - -
NeedsEscapingCurrent 4 -1 6.112 ns 0.1563 ns 0.1462 ns 6.090 ns 5.873 ns 6.453 ns 1.00 0.00 - - - -
NeedsEscapingNew 4 -1 6.996 ns 0.1713 ns 0.1602 ns 6.957 ns 6.767 ns 7.358 ns 1.15 0.04 - - - -
NeedsEscapingNewFixed 4 -1 7.931 ns 0.1660 ns 0.1472 ns 7.898 ns 7.655 ns 8.202 ns 1.30 0.04 - - - -
NeedsEscapingNewFixedLoopUnrolled 4 -1 8.661 ns 0.4779 ns 0.4907 ns 8.684 ns 7.757 ns 9.520 ns 1.43 0.09 - - - -
NeedsEscapingNewLoopUnrolled 4 -1 7.151 ns 0.3808 ns 0.4386 ns 7.246 ns 6.417 ns 7.848 ns 1.17 0.07 - - - -
NeedsEscapingNewDoWhile 4 -1 7.241 ns 0.1767 ns 0.2035 ns 7.164 ns 6.919 ns 7.710 ns 1.19 0.05 - - - -
NeedsEscapingBackFill 4 -1 9.702 ns 0.2369 ns 0.2216 ns 9.630 ns 9.487 ns 10.171 ns 1.59 0.05 - - - -
NeedsEscapingJumpTable 4 -1 7.069 ns 0.2705 ns 0.2777 ns 7.003 ns 6.689 ns 7.665 ns 1.15 0.05 - - - -
NeedsEscapingCurrent 5 -1 7.132 ns 0.1642 ns 0.1686 ns 7.095 ns 6.872 ns 7.403 ns 1.00 0.00 - - - -
NeedsEscapingNew 5 -1 8.008 ns 0.1085 ns 0.0962 ns 7.993 ns 7.912 ns 8.224 ns 1.12 0.03 - - - -
NeedsEscapingNewFixed 5 -1 9.002 ns 0.1914 ns 0.1697 ns 8.965 ns 8.726 ns 9.341 ns 1.26 0.03 - - - -
NeedsEscapingNewFixedLoopUnrolled 5 -1 9.606 ns 0.4229 ns 0.4871 ns 9.447 ns 9.002 ns 10.656 ns 1.33 0.06 - - - -
NeedsEscapingNewLoopUnrolled 5 -1 7.409 ns 0.1152 ns 0.1021 ns 7.394 ns 7.284 ns 7.658 ns 1.04 0.03 - - - -
NeedsEscapingNewDoWhile 5 -1 7.688 ns 0.1525 ns 0.1274 ns 7.710 ns 7.421 ns 7.907 ns 1.08 0.03 - - - -
NeedsEscapingBackFill 5 -1 10.224 ns 1.0558 ns 1.1735 ns 9.640 ns 9.370 ns 12.842 ns 1.44 0.19 - - - -
NeedsEscapingJumpTable 5 -1 7.682 ns 0.2026 ns 0.1895 ns 7.622 ns 7.487 ns 8.136 ns 1.08 0.04 - - - -
NeedsEscapingCurrent 6 -1 8.645 ns 0.4273 ns 0.4388 ns 8.467 ns 8.242 ns 9.955 ns 1.00 0.00 - - - -
NeedsEscapingNew 6 -1 9.664 ns 0.5852 ns 0.5747 ns 9.464 ns 9.053 ns 10.704 ns 1.13 0.08 - - - -
NeedsEscapingNewFixed 6 -1 10.479 ns 0.2413 ns 0.2779 ns 10.450 ns 10.062 ns 11.007 ns 1.22 0.06 - - - -
NeedsEscapingNewFixedLoopUnrolled 6 -1 10.164 ns 0.1712 ns 0.1601 ns 10.134 ns 9.955 ns 10.450 ns 1.19 0.04 - - - -
NeedsEscapingNewLoopUnrolled 6 -1 8.560 ns 0.1726 ns 0.1615 ns 8.532 ns 8.327 ns 8.811 ns 1.00 0.04 - - - -
NeedsEscapingNewDoWhile 6 -1 9.156 ns 0.2517 ns 0.2898 ns 9.066 ns 8.800 ns 9.831 ns 1.06 0.06 - - - -
NeedsEscapingBackFill 6 -1 9.743 ns 0.2955 ns 0.2765 ns 9.699 ns 9.424 ns 10.539 ns 1.14 0.05 - - - -
NeedsEscapingJumpTable 6 -1 9.344 ns 0.4587 ns 0.5282 ns 9.496 ns 8.538 ns 10.139 ns 1.10 0.09 - - - -
NeedsEscapingCurrent 7 -1 9.856 ns 0.2589 ns 0.2770 ns 9.869 ns 9.447 ns 10.544 ns 1.00 0.00 - - - -
NeedsEscapingNew 7 -1 10.595 ns 0.2594 ns 0.2883 ns 10.591 ns 10.197 ns 11.424 ns 1.08 0.04 - - - -
NeedsEscapingNewFixed 7 -1 11.372 ns 0.2577 ns 0.2410 ns 11.417 ns 10.941 ns 11.799 ns 1.16 0.04 - - - -
NeedsEscapingNewFixedLoopUnrolled 7 -1 11.134 ns 0.2799 ns 0.3223 ns 11.122 ns 10.705 ns 11.747 ns 1.13 0.05 - - - -
NeedsEscapingNewLoopUnrolled 7 -1 9.826 ns 0.2273 ns 0.2232 ns 9.780 ns 9.437 ns 10.232 ns 1.00 0.03 - - - -
NeedsEscapingNewDoWhile 7 -1 10.390 ns 0.2352 ns 0.2709 ns 10.460 ns 9.927 ns 10.934 ns 1.06 0.03 - - - -
NeedsEscapingBackFill 7 -1 10.215 ns 0.2522 ns 0.2803 ns 10.125 ns 9.918 ns 10.831 ns 1.04 0.05 - - - -
NeedsEscapingJumpTable 7 -1 10.696 ns 1.2906 ns 1.3809 ns 10.162 ns 9.322 ns 13.859 ns 1.09 0.15 - - - -
NeedsEscapingCurrent 8 -1 10.703 ns 0.1520 ns 0.1348 ns 10.696 ns 10.499 ns 10.976 ns 1.00 0.00 - - - -
NeedsEscapingNew 8 -1 9.380 ns 0.1847 ns 0.1637 ns 9.339 ns 9.168 ns 9.727 ns 0.88 0.01 - - - -
NeedsEscapingNewFixed 8 -1 9.394 ns 0.1215 ns 0.1015 ns 9.375 ns 9.221 ns 9.607 ns 0.88 0.02 - - - -
NeedsEscapingNewFixedLoopUnrolled 8 -1 9.560 ns 0.2111 ns 0.2073 ns 9.541 ns 9.211 ns 10.053 ns 0.89 0.02 - - - -
NeedsEscapingNewLoopUnrolled 8 -1 8.124 ns 0.1944 ns 0.1996 ns 8.109 ns 7.869 ns 8.603 ns 0.76 0.02 - - - -
NeedsEscapingNewDoWhile 8 -1 8.057 ns 0.1944 ns 0.1818 ns 8.019 ns 7.803 ns 8.414 ns 0.75 0.02 - - - -
NeedsEscapingBackFill 8 -1 9.992 ns 0.3363 ns 0.3454 ns 9.851 ns 9.542 ns 10.751 ns 0.93 0.03 - - - -
NeedsEscapingJumpTable 8 -1 8.946 ns 0.2250 ns 0.2591 ns 8.991 ns 8.501 ns 9.478 ns 0.84 0.02 - - - -
NeedsEscapingCurrent 9 -1 11.998 ns 0.3648 ns 0.4054 ns 11.969 ns 11.224 ns 12.792 ns 1.00 0.00 - - - -
NeedsEscapingNew 9 -1 8.975 ns 0.4747 ns 0.5466 ns 8.748 ns 8.373 ns 10.005 ns 0.75 0.06 - - - -
NeedsEscapingNewFixed 9 -1 10.123 ns 0.1938 ns 0.1618 ns 10.074 ns 9.907 ns 10.519 ns 0.84 0.03 - - - -
NeedsEscapingNewFixedLoopUnrolled 9 -1 10.284 ns 0.2768 ns 0.3188 ns 10.185 ns 9.858 ns 10.912 ns 0.86 0.05 - - - -
NeedsEscapingNewLoopUnrolled 9 -1 8.787 ns 0.1823 ns 0.1705 ns 8.795 ns 8.502 ns 9.080 ns 0.72 0.03 - - - -
NeedsEscapingNewDoWhile 9 -1 8.604 ns 0.1877 ns 0.1756 ns 8.552 ns 8.338 ns 8.970 ns 0.71 0.03 - - - -
NeedsEscapingBackFill 9 -1 14.387 ns 0.3873 ns 0.3623 ns 14.333 ns 13.976 ns 15.113 ns 1.19 0.05 - - - -
NeedsEscapingJumpTable 9 -1 19.150 ns 0.3943 ns 0.3495 ns 19.058 ns 18.693 ns 19.721 ns 1.58 0.06 - - - -
NeedsEscapingCurrent 15 -1 18.265 ns 0.3691 ns 0.3625 ns 18.249 ns 17.808 ns 18.958 ns 1.00 0.00 - - - -
NeedsEscapingNew 15 -1 14.593 ns 0.2999 ns 0.2806 ns 14.566 ns 14.115 ns 15.075 ns 0.80 0.02 - - - -
NeedsEscapingNewFixed 15 -1 15.311 ns 0.3182 ns 0.2484 ns 15.361 ns 14.820 ns 15.565 ns 0.84 0.02 - - - -
NeedsEscapingNewFixedLoopUnrolled 15 -1 15.564 ns 0.3684 ns 0.3446 ns 15.434 ns 15.004 ns 16.223 ns 0.85 0.03 - - - -
NeedsEscapingNewLoopUnrolled 15 -1 15.231 ns 0.6932 ns 0.7983 ns 15.375 ns 13.855 ns 16.701 ns 0.85 0.04 - - - -
NeedsEscapingNewDoWhile 15 -1 14.292 ns 0.2765 ns 0.2587 ns 14.214 ns 13.788 ns 14.711 ns 0.78 0.02 - - - -
NeedsEscapingBackFill 15 -1 15.596 ns 1.2351 ns 1.3215 ns 15.167 ns 14.495 ns 19.385 ns 0.86 0.07 - - - -
NeedsEscapingJumpTable 15 -1 25.996 ns 0.5618 ns 0.6245 ns 26.106 ns 24.845 ns 27.367 ns 1.43 0.04 - - - -
NeedsEscapingCurrent 16 -1 19.222 ns 0.3942 ns 0.4048 ns 19.083 ns 18.740 ns 20.062 ns 1.00 0.00 - - - -
NeedsEscapingNew 16 -1 12.795 ns 0.2610 ns 0.2442 ns 12.789 ns 12.428 ns 13.171 ns 0.67 0.02 - - - -
NeedsEscapingNewFixed 16 -1 10.735 ns 0.2731 ns 0.2805 ns 10.650 ns 10.438 ns 11.444 ns 0.56 0.02 - - - -
NeedsEscapingNewFixedLoopUnrolled 16 -1 10.974 ns 0.2475 ns 0.2751 ns 10.903 ns 10.557 ns 11.669 ns 0.57 0.02 - - - -
NeedsEscapingNewLoopUnrolled 16 -1 13.235 ns 0.2002 ns 0.1775 ns 13.231 ns 12.958 ns 13.597 ns 0.69 0.01 - - - -
NeedsEscapingNewDoWhile 16 -1 12.966 ns 0.1866 ns 0.1746 ns 12.961 ns 12.736 ns 13.370 ns 0.67 0.01 - - - -
NeedsEscapingBackFill 16 -1 11.303 ns 0.1761 ns 0.1561 ns 11.315 ns 11.092 ns 11.647 ns 0.59 0.02 - - - -
NeedsEscapingJumpTable 16 -1 10.694 ns 0.1528 ns 0.1429 ns 10.705 ns 10.377 ns 10.911 ns 0.56 0.01 - - - -
NeedsEscapingCurrent 17 -1 20.404 ns 0.3783 ns 0.3539 ns 20.450 ns 19.807 ns 20.995 ns 1.00 0.00 - - - -
NeedsEscapingNew 17 -1 13.755 ns 0.5308 ns 0.6113 ns 13.457 ns 13.037 ns 14.559 ns 0.66 0.02 - - - -
NeedsEscapingNewFixed 17 -1 11.912 ns 0.2521 ns 0.2358 ns 11.892 ns 11.527 ns 12.383 ns 0.58 0.02 - - - -
NeedsEscapingNewFixedLoopUnrolled 17 -1 11.895 ns 0.1426 ns 0.1334 ns 11.826 ns 11.747 ns 12.185 ns 0.58 0.01 - - - -
NeedsEscapingNewLoopUnrolled 17 -1 13.919 ns 0.1850 ns 0.1640 ns 13.860 ns 13.753 ns 14.312 ns 0.68 0.02 - - - -
NeedsEscapingNewDoWhile 17 -1 13.041 ns 0.1467 ns 0.1225 ns 13.104 ns 12.805 ns 13.235 ns 0.64 0.01 - - - -
NeedsEscapingBackFill 17 -1 16.251 ns 0.2278 ns 0.2131 ns 16.230 ns 15.997 ns 16.671 ns 0.80 0.02 - - - -
NeedsEscapingJumpTable 17 -1 29.062 ns 0.4583 ns 0.4287 ns 29.055 ns 28.207 ns 29.829 ns 1.42 0.04 - - - -
NeedsEscapingCurrent 100 -1 119.098 ns 1.5810 ns 1.4016 ns 119.111 ns 117.178 ns 122.534 ns 1.00 0.00 - - - -
NeedsEscapingNew 100 -1 65.558 ns 0.7238 ns 0.6770 ns 65.586 ns 64.564 ns 66.549 ns 0.55 0.01 - - - -
NeedsEscapingNewFixed 100 -1 35.235 ns 0.5803 ns 0.4845 ns 35.304 ns 34.227 ns 36.049 ns 0.30 0.01 - - - -
NeedsEscapingNewFixedLoopUnrolled 100 -1 36.180 ns 0.6836 ns 0.6394 ns 36.441 ns 34.949 ns 36.941 ns 0.30 0.01 - - - -
NeedsEscapingNewLoopUnrolled 100 -1 69.195 ns 0.8595 ns 0.8040 ns 69.077 ns 68.105 ns 70.342 ns 0.58 0.01 - - - -
NeedsEscapingNewDoWhile 100 -1 66.882 ns 0.8437 ns 0.7892 ns 66.967 ns 65.798 ns 68.058 ns 0.56 0.01 - - - -
NeedsEscapingBackFill 100 -1 42.295 ns 0.5550 ns 0.5191 ns 42.435 ns 41.131 ns 43.107 ns 0.36 0.00 - - - -
NeedsEscapingJumpTable 100 -1 147.120 ns 2.1331 ns 1.9953 ns 147.572 ns 142.867 ns 149.873 ns 1.24 0.02 - - - -
NeedsEscapingCurrent 1000 -1 1,117.042 ns 14.0214 ns 13.1157 ns 1,124.155 ns 1,090.461 ns 1,136.772 ns 1.00 0.00 - - - -
NeedsEscapingNew 1000 -1 638.282 ns 7.6395 ns 6.7723 ns 639.820 ns 625.899 ns 648.740 ns 0.57 0.01 - - - -
NeedsEscapingNewFixed 1000 -1 282.238 ns 3.0423 ns 2.8458 ns 282.323 ns 276.458 ns 286.061 ns 0.25 0.00 - - - -
NeedsEscapingNewFixedLoopUnrolled 1000 -1 277.703 ns 2.5134 ns 2.3510 ns 278.150 ns 273.548 ns 281.317 ns 0.25 0.00 - - - -
NeedsEscapingNewLoopUnrolled 1000 -1 663.711 ns 8.3906 ns 7.8485 ns 664.428 ns 650.036 ns 679.634 ns 0.59 0.01 - - - -
NeedsEscapingNewDoWhile 1000 -1 650.888 ns 64.6500 ns 74.4510 ns 607.930 ns 583.460 ns 781.742 ns 0.59 0.07 - - - -
NeedsEscapingBackFill 1000 -1 282.575 ns 4.7633 ns 4.2226 ns 282.683 ns 272.751 ns 287.361 ns 0.25 0.00 - - - -
NeedsEscapingJumpTable 1000 -1 277.898 ns 5.3987 ns 5.3023 ns 278.105 ns 268.696 ns 287.810 ns 0.25 0.01 - - - -
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment