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
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
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;
}
- Instrinsics (with fixed) + loop-unrolling at the end
- Intrinsics (without fixed)
- Intrinsics (without fixed) + loop-unrolling at the end
- Intrinsics (without fixed) using do-while + loop-unrolling at the end
- Intrinsics (with fixed) + custom loop-unrolling reading 4 chars into ulong, 2 chars into a uint
- 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)
- Intrinsics (with fixed) with initial jump table, and "leftover" loop
- 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.
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 | - | - | - | - |
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 |
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 |
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).
- For end-to-end scenario (such as serializing commonly found objects/payloads), there is a 10-20% improvement.
- Writing relatively large JSON strings using the writer got ~30% faster (i.e. greater than 16 characters).
- 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.
- 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).
- Add similar support and tests for UTF-8 bytes, not just UTF-16 characters.
- Evaluate if the trade-off is worth it for property names which tend to be small (2-8 characters), compared to values.
- Consider optimizing the commonly used built-in
JavascriptEncoder
statics using similar techniques. - Apply non-Sse2 based optimizations where Sse2 isn't supported rather than processing one character at a time.
- 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.