diff --git a/Benchmark/ExtensionMethods.cs b/Benchmark/ExtensionMethods.cs index c70b4ea2..72b5caa3 100644 --- a/Benchmark/ExtensionMethods.cs +++ b/Benchmark/ExtensionMethods.cs @@ -14,22 +14,12 @@ public interface IGenericEquality static class ExtensionMethods { - static readonly char[] ASCII; - - static ExtensionMethods() - { - var cs = new List(); - - for (var i = 0; i <= byte.MaxValue; i++) - { - var c = (char)i; - if (char.IsControl(c)) continue; - - cs.Add(c); - } - - ASCII = cs.ToArray(); - } + static readonly char[] ASCII = + Enumerable + .Range(0, byte.MaxValue) + .Select(b => (char)b) + .Where(c => !Char.IsControl(c)) + .ToArray(); public static bool TrueEqualsDictionary(this Dictionary a, Dictionary b) where V : class, IGenericEquality diff --git a/Benchmark/Result.cs b/Benchmark/Result.cs index d2b9ab76..61046f60 100644 --- a/Benchmark/Result.cs +++ b/Benchmark/Result.cs @@ -11,5 +11,6 @@ class Result public string Serializer { get; set; } public string TypeName { get; set; } public TimeSpan Elapsed { get; set; } + public int[] GCCounts { get; set; } } } diff --git a/Jil/Serialize/InlineSerializer.cs b/Jil/Serialize/InlineSerializer.cs index 2666d159..520ff9a0 100644 --- a/Jil/Serialize/InlineSerializer.cs +++ b/Jil/Serialize/InlineSerializer.cs @@ -13,25 +13,24 @@ namespace Jil.Serialize { - class InlineSerializer - { - public static bool ReorderMembers = true; - public static bool UseCustomIntegerToString = true; - public static bool SkipDateTimeMathMethods = true; - public static bool UseCustomISODateFormatting = true; - public static bool UseFastLists = true; - public static bool UseFastArrays = true; - public static bool UseFastGuids = true; - public static bool AllocationlessDictionaries = true; - public static bool PropagateConstants = true; - public static bool UseCustomWriteIntUnrolled = true; - public static bool UseCustomRFC1123DateTimeFormatting = true; - - static string CharBuffer = "char_buffer"; - internal const int CharBufferSize = 36; - internal const int RecursionLimit = 50; - - static Dictionary CharacterEscapes = + internal static class InlineSerializer + { + /// + /// Name used for character buffer local variables. + /// + public const string CharBuffer = "char_buffer"; + + /// + /// The maximum length of the character buffer needed to serialize any primitive type. + /// + public const int CharBufferSize = 36; + + public const int RecursionLimit = 50; + + /// + /// Escape sequences for special characters. + /// + public static readonly Dictionary CharacterEscapes = new Dictionary { { '\\', @"\\" }, @@ -69,6 +68,22 @@ class InlineSerializer { '\u001E', @"\u001E" }, { '\u001F', @"\u001F" } }; + } + + class InlineSerializer + { + public static bool ReorderMembers = true; + public static bool UseCustomIntegerToString = true; + public static bool SkipDateTimeMathMethods = true; + public static bool UseCustomISODateFormatting = true; + public static bool UseFastLists = true; + public static bool UseFastArrays = true; + public static bool UseFastGuids = true; + public static bool AllocationlessDictionaries = true; + public static bool PropagateConstants = true; + public static bool UseCustomWriteIntUnrolled = true; + public static bool UseCustomRFC1123DateTimeFormatting = true; + public static bool UseCachedCharBuffers = true; private readonly Type RecursionLookupOptionsType; // This is a type that implements ISerializeOptions and has an empty, public constructor private readonly bool ExcludeNulls; @@ -791,8 +806,16 @@ void WriteISO8601StyleDateTime() return; } - Emit.LoadLocal(CharBuffer); // TextWriter DateTime char[] - Emit.Call(Methods.GetCustomISO8601ToString(BuildingToString)); // --empty-- + if (UseCachedCharBuffers) + { + Emit.Call(Methods.GetCachedCharBuffer()); // TextWriter DateTime char[] + } + else + { + Emit.LoadLocal(InlineSerializer.CharBuffer); // TextWriter DateTime char[] + } + + Emit.Call(Methods.GetCustomISO8601ToString(BuildingToString)); // --empty-- } static readonly MethodInfo DateTime_ToString = typeof(DateTime).GetMethod("ToString", new[] { typeof(string) }); @@ -866,7 +889,14 @@ void WriteDateTimeOffset() Emit.Call(TimeSpan_Minutes); // TextWriter long int int } - Emit.LoadLocal(CharBuffer); // TextWriter long int int char[] + if (UseCachedCharBuffers) + { + Emit.Call(Methods.GetCachedCharBuffer()); // TextWriter long int int char[] + } + else + { + Emit.LoadLocal(InlineSerializer.CharBuffer); // TextWriter long int int char[] + } Emit.Call(Methods.GetCustomWriteMicrosoftStyleWithOffset(BuildingToString)); // --empty-- return; } @@ -888,7 +918,15 @@ void WriteDateTimeOffset() Emit.LoadLocalAddress(tsLoc); // TextWriter DateTime int TimeSpan* Emit.Call(TimeSpan_Minutes); // TextWriter DateTime int int } - Emit.LoadLocal(CharBuffer); // TextWriter DateTime int int char[] + + if (UseCachedCharBuffers) + { + Emit.Call(Methods.GetCachedCharBuffer()); // TextWriter DateTime int int char[] + } + else + { + Emit.LoadLocal(InlineSerializer.CharBuffer); // TextWriter DateTime int int char[] + } Emit.Call(Methods.GetCustomISO8601WithOffsetToString(BuildingToString)); // --empty-- return; } @@ -930,7 +968,14 @@ void WriteTimeSpan() return; } - Emit.LoadLocal(CharBuffer); // TextWriter TimeSpan char[] + if (UseCachedCharBuffers) + { + Emit.Call(Methods.GetCachedCharBuffer()); // TextWriter TimeSpan char[] + } + else + { + Emit.LoadLocal(InlineSerializer.CharBuffer); // TextWriter TimeSpan char[] + } switch(DateFormat) { @@ -1114,32 +1159,60 @@ void WritePrimitive(Type primitiveType, bool quotesNeedHandling) { if (primitiveType == typeof(int)) { - Emit.LoadLocal(CharBuffer); // TextWriter int char[] - CallWriteInt(); // --empty-- + if (UseCachedCharBuffers) + { + Emit.Call(Methods.GetCachedCharBuffer()); // TextWriter int char[] + } + else + { + Emit.LoadLocal(InlineSerializer.CharBuffer); // TextWriter int char[] + } + CallWriteInt(); // --empty-- return; } if (primitiveType == typeof(uint)) { - Emit.LoadLocal(CharBuffer); // TextWriter int char[] - CallWriteUInt(); // --empty-- + if (UseCachedCharBuffers) + { + Emit.Call(Methods.GetCachedCharBuffer()); // TextWriter uint char[] + } + else + { + Emit.LoadLocal(InlineSerializer.CharBuffer); // TextWriter uint char[] + } + CallWriteUInt(); // --empty-- return; } if (primitiveType == typeof(long)) { - Emit.LoadLocal(CharBuffer); // TextWriter int char[] - CallWriteLong(); // --empty-- + if (UseCachedCharBuffers) + { + Emit.Call(Methods.GetCachedCharBuffer()); // TextWriter long char[] + } + else + { + Emit.LoadLocal(InlineSerializer.CharBuffer); // TextWriter long char[] + } + CallWriteLong(); // --empty-- return; } if (primitiveType == typeof(ulong)) { - Emit.LoadLocal(CharBuffer); // TextWriter int char[] - CallWriteULong(); // --empty-- + if (UseCachedCharBuffers) + { + Emit.Call(Methods.GetCachedCharBuffer()); // TextWriter ulong char[] + } + else + { + Emit.LoadLocal(InlineSerializer.CharBuffer); // TextWriter ulong char[] + } + CallWriteULong(); // --empty-- return; } @@ -1213,15 +1286,22 @@ void WriteGuidFast(bool quotesNeedHandling) { if (quotesNeedHandling) { - WriteString("\""); // TextWriter Guid + WriteString("\""); // TextWriter Guid } - Emit.LoadLocal(CharBuffer); // TextWriter Guid char[] - Emit.Call(Methods.GetWriteGuid(BuildingToString)); // --empty-- + if (UseCachedCharBuffers) + { + Emit.Call(Methods.GetCachedCharBuffer()); // TextWriter Guid char[] + } + else + { + Emit.LoadLocal(InlineSerializer.CharBuffer); // TextWriter Guid char[] + } + Emit.Call(Methods.GetWriteGuid(BuildingToString)); // --empty-- if (quotesNeedHandling) { - WriteString("\""); // --empty-- + WriteString("\""); // --empty-- } } @@ -1284,9 +1364,9 @@ void WriteEncodedChar(bool quotesNeedHandling) writeChar = typeof(TextWriter).GetMethod("Write", new[] { typeof(char) }); } - var lowestCharNeedingEncoding = (int)CharacterEscapes.Keys.OrderBy(c => (int)c).First(); + var lowestCharNeedingEncoding = (int)InlineSerializer.CharacterEscapes.Keys.OrderBy(c => (int)c).First(); - var needLabels = CharacterEscapes.OrderBy(kv => kv.Key).Select(kv => Tuple.Create(kv.Key - lowestCharNeedingEncoding, kv.Value)).ToList(); + var needLabels = InlineSerializer.CharacterEscapes.OrderBy(kv => kv.Key).Select(kv => Tuple.Create(kv.Key - lowestCharNeedingEncoding, kv.Value)).ToList(); var labels = new List>(); @@ -3508,10 +3588,10 @@ void AddCharBuffer(Type serializingType) return; } - Emit.DeclareLocal(CharBuffer); - Emit.LoadConstant(CharBufferSize); + Emit.DeclareLocal(InlineSerializer.CharBuffer); + Emit.LoadConstant(InlineSerializer.CharBufferSize); Emit.NewArray(); - Emit.StoreLocal(CharBuffer); + Emit.StoreLocal(InlineSerializer.CharBuffer); } Emit MakeEmit(Type forType) @@ -3538,9 +3618,9 @@ void BuildObjectWithNewImpl() { var goOn = Emit.DefineLabel(); - Emit.LoadArgument(2); // int - Emit.LoadConstant(RecursionLimit); // int int - Emit.BranchIfLess(goOn); // --empty-- + Emit.LoadArgument(2); // int + Emit.LoadConstant(InlineSerializer.RecursionLimit); // int int + Emit.BranchIfLess(goOn); // --empty-- Emit.NewObject(typeof(InfiniteRecursionException)); // infiniteRecursionException Emit.Throw(); // --empty-- @@ -3548,7 +3628,10 @@ void BuildObjectWithNewImpl() Emit.MarkLabel(goOn); // --empty-- } - AddCharBuffer(typeof(ForType)); + if (!UseCachedCharBuffers) + { + AddCharBuffer(typeof(ForType)); + } RecursiveTypes = PreloadRecursiveTypes(recursiveTypes); @@ -3605,7 +3688,10 @@ void BuildListWithNewImpl(MemberInfo dynamicMember) Emit = MakeEmit(typeof(ForType)); - AddCharBuffer(typeof(ForType)); + if (!UseCachedCharBuffers) + { + AddCharBuffer(typeof(ForType)); + } RecursiveTypes = PreloadRecursiveTypes(recursiveTypes); @@ -3633,7 +3719,10 @@ void BuildEnumerableWithNewImpl(MemberInfo dynamicMember) Emit = MakeEmit(typeof(ForType)); - AddCharBuffer(typeof(ForType)); + if (!UseCachedCharBuffers) + { + AddCharBuffer(typeof(ForType)); + } RecursiveTypes = PreloadRecursiveTypes(recursiveTypes); @@ -3661,7 +3750,10 @@ void BuildDictionaryWithNewImpl(MemberInfo dynamicMember) Emit = MakeEmit(typeof(ForType)); - AddCharBuffer(typeof(ForType)); + if (!UseCachedCharBuffers) + { + AddCharBuffer(typeof(ForType)); + } RecursiveTypes = PreloadRecursiveTypes(recursiveTypes); @@ -3687,7 +3779,10 @@ void BuildPrimitiveWithNewImpl() { Emit = MakeEmit(typeof(ForType)); - AddCharBuffer(typeof(ForType)); + if (!UseCachedCharBuffers) + { + AddCharBuffer(typeof(ForType)); + } Emit.LoadArgument(0); Emit.LoadArgument(1); @@ -3717,7 +3812,10 @@ void BuildNullableWithNewImpl(MemberInfo dynamicMember) Emit = MakeEmit(typeof(ForType)); - AddCharBuffer(typeof(ForType)); + if (!UseCachedCharBuffers) + { + AddCharBuffer(typeof(ForType)); + } RecursiveTypes = PreloadRecursiveTypes(recursiveTypes); diff --git a/Jil/Serialize/Methods.Get.cs b/Jil/Serialize/Methods.Get.cs index 2c4c8753..cd51ca4a 100644 --- a/Jil/Serialize/Methods.Get.cs +++ b/Jil/Serialize/Methods.Get.cs @@ -9,6 +9,12 @@ namespace Jil.Serialize { static partial class Methods { + public static MethodInfo GetCachedCharBuffer() + { + // no difference depending on thunk writer here + return GetThreadLocalCharBuffer; + } + public static MethodInfo GetValidateDouble() { // no difference depending on thunk writer here diff --git a/Jil/Serialize/Methods.ThunkWriter.cs b/Jil/Serialize/Methods.ThunkWriter.cs index 044a20f3..35cc1a7e 100644 --- a/Jil/Serialize/Methods.ThunkWriter.cs +++ b/Jil/Serialize/Methods.ThunkWriter.cs @@ -782,7 +782,7 @@ static void _CustomWriteInt_ThunkWriter(ref ThunkWriter writer, int number, char return; } - var ptr = InlineSerializer.CharBufferSize - 1; + var ptr = InlineSerializer.CharBufferSize - 1; uint copy; if (number < 0) @@ -810,7 +810,7 @@ static void _CustomWriteInt_ThunkWriter(ref ThunkWriter writer, int number, char ptr++; } - writer.Write(buffer, ptr + 1, InlineSerializer.CharBufferSize - 1 - ptr); + writer.Write(buffer, ptr + 1, InlineSerializer.CharBufferSize - 1 - ptr); } static readonly MethodInfo CustomWriteIntUnrolledSigned_ThunkWriter = typeof(Methods).GetMethod("_CustomWriteIntUnrolledSigned_ThunkWriter", BindingFlags.Static | BindingFlags.NonPublic); @@ -970,7 +970,7 @@ static void _CustomWriteIntUnrolledSigned_ThunkWriter(ref ThunkWriter writer, in [MethodImpl(MethodImplOptions.AggressiveInlining)] static void _CustomWriteUInt_ThunkWriter(ref ThunkWriter writer, uint number, char[] buffer) { - var ptr = InlineSerializer.CharBufferSize - 1; + var ptr = InlineSerializer.CharBufferSize - 1; var copy = number; @@ -989,7 +989,7 @@ static void _CustomWriteUInt_ThunkWriter(ref ThunkWriter writer, uint number, ch ptr++; } - writer.Write(buffer, ptr + 1, InlineSerializer.CharBufferSize - 1 - ptr); + writer.Write(buffer, ptr + 1, InlineSerializer.CharBufferSize - 1 - ptr); } static readonly MethodInfo CustomWriteUIntUnrolled_ThunkWriter = typeof(Methods).GetMethod("_CustomWriteUIntUnrolled_ThunkWriter", BindingFlags.Static | BindingFlags.NonPublic); @@ -1109,7 +1109,7 @@ static void _CustomWriteLong_ThunkWriter(ref ThunkWriter writer, long number, ch return; } - var ptr = InlineSerializer.CharBufferSize - 1; + var ptr = InlineSerializer.CharBufferSize - 1; ulong copy; if (number < 0) @@ -1137,14 +1137,14 @@ static void _CustomWriteLong_ThunkWriter(ref ThunkWriter writer, long number, ch ptr++; } - writer.Write(buffer, ptr + 1, InlineSerializer.CharBufferSize - 1 - ptr); + writer.Write(buffer, ptr + 1, InlineSerializer.CharBufferSize - 1 - ptr); } static readonly MethodInfo CustomWriteULong_ThunkWriter = typeof(Methods).GetMethod("_CustomWriteULong_ThunkWriter", BindingFlags.Static | BindingFlags.NonPublic); [MethodImpl(MethodImplOptions.AggressiveInlining)] static void _CustomWriteULong_ThunkWriter(ref ThunkWriter writer, ulong number, char[] buffer) { - var ptr = InlineSerializer.CharBufferSize - 1; + var ptr = InlineSerializer.CharBufferSize - 1; var copy = number; @@ -1163,7 +1163,7 @@ static void _CustomWriteULong_ThunkWriter(ref ThunkWriter writer, ulong number, ptr++; } - writer.Write(buffer, ptr + 1, InlineSerializer.CharBufferSize - 1 - ptr); + writer.Write(buffer, ptr + 1, InlineSerializer.CharBufferSize - 1 - ptr); } static readonly MethodInfo ProxyFloat_ThunkWriter = typeof(Methods).GetMethod("_ProxyFloat_ThunkWriter", BindingFlags.Static | BindingFlags.NonPublic); diff --git a/Jil/Serialize/Methods.cs b/Jil/Serialize/Methods.cs index 6be65665..50d83350 100644 --- a/Jil/Serialize/Methods.cs +++ b/Jil/Serialize/Methods.cs @@ -6,13 +6,30 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; +using System.Threading; namespace Jil.Serialize { static partial class Methods { + /// + /// Thread-local, cached character buffers. + /// + /// + /// Caching the character buffers improves performance by avoiding allocation + /// (and subsequent collection) of a short-lived array each time an instance/value + /// of a primitive type is serialized. + /// + [ThreadStatic] + private static char[] _threadLocalCharBuffer; + + static readonly MethodInfo GetThreadLocalCharBuffer = typeof(Methods).GetMethod("_GetThreadLocalCharBuffer", BindingFlags.Static | BindingFlags.NonPublic); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static char[] _GetThreadLocalCharBuffer() + { + return _threadLocalCharBuffer ?? (_threadLocalCharBuffer = new char[InlineSerializer.CharBufferSize]); + } + struct TwoDigits { public readonly char First; @@ -866,7 +883,7 @@ static void _CustomWriteInt(TextWriter writer, int number, char[] buffer) return; } - var ptr = InlineSerializer.CharBufferSize - 1; + var ptr = InlineSerializer.CharBufferSize - 1; uint copy; if (number < 0) @@ -894,7 +911,7 @@ static void _CustomWriteInt(TextWriter writer, int number, char[] buffer) ptr++; } - writer.Write(buffer, ptr + 1, InlineSerializer.CharBufferSize - 1 - ptr); + writer.Write(buffer, ptr + 1, InlineSerializer.CharBufferSize - 1 - ptr); } static readonly MethodInfo CustomWriteIntUnrolledSigned = typeof(Methods).GetMethod("_CustomWriteIntUnrolledSigned", BindingFlags.Static | BindingFlags.NonPublic); @@ -1054,7 +1071,7 @@ static void _CustomWriteIntUnrolledSigned(TextWriter writer, int num, char[] buf [MethodImpl(MethodImplOptions.AggressiveInlining)] static void _CustomWriteUInt(TextWriter writer, uint number, char[] buffer) { - var ptr = InlineSerializer.CharBufferSize - 1; + var ptr = InlineSerializer.CharBufferSize - 1; var copy = number; @@ -1073,7 +1090,7 @@ static void _CustomWriteUInt(TextWriter writer, uint number, char[] buffer) ptr++; } - writer.Write(buffer, ptr + 1, InlineSerializer.CharBufferSize - 1 - ptr); + writer.Write(buffer, ptr + 1, InlineSerializer.CharBufferSize - 1 - ptr); } static readonly MethodInfo CustomWriteUIntUnrolled = typeof(Methods).GetMethod("_CustomWriteUIntUnrolled", BindingFlags.Static | BindingFlags.NonPublic); @@ -1193,7 +1210,7 @@ static void _CustomWriteLong(TextWriter writer, long number, char[] buffer) return; } - var ptr = InlineSerializer.CharBufferSize - 1; + var ptr = InlineSerializer.CharBufferSize - 1; ulong copy; if (number < 0) @@ -1221,14 +1238,14 @@ static void _CustomWriteLong(TextWriter writer, long number, char[] buffer) ptr++; } - writer.Write(buffer, ptr + 1, InlineSerializer.CharBufferSize - 1 - ptr); + writer.Write(buffer, ptr + 1, InlineSerializer.CharBufferSize - 1 - ptr); } static readonly MethodInfo CustomWriteULong = typeof(Methods).GetMethod("_CustomWriteULong", BindingFlags.Static | BindingFlags.NonPublic); [MethodImpl(MethodImplOptions.AggressiveInlining)] static void _CustomWriteULong(TextWriter writer, ulong number, char[] buffer) { - var ptr = InlineSerializer.CharBufferSize - 1; + var ptr = InlineSerializer.CharBufferSize - 1; var copy = number; @@ -1247,7 +1264,7 @@ static void _CustomWriteULong(TextWriter writer, ulong number, char[] buffer) ptr++; } - writer.Write(buffer, ptr + 1, InlineSerializer.CharBufferSize - 1 - ptr); + writer.Write(buffer, ptr + 1, InlineSerializer.CharBufferSize - 1 - ptr); } static readonly MethodInfo ProxyFloat = typeof(Methods).GetMethod("_ProxyFloat", BindingFlags.Static | BindingFlags.NonPublic);