From bf1029bd1c3393fafd66de4cbd6d7132ae5e50dc Mon Sep 17 00:00:00 2001 From: Loic Denuziere Date: Sat, 10 Aug 2019 16:24:08 +0200 Subject: [PATCH 1/8] Add BenchmarkDotNet.Diagnostics.Windows for perfview --- benchmarks/FSharp.SystemTextJson.Benchmarks/paket.references | 3 ++- paket.dependencies | 1 + paket.lock | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/benchmarks/FSharp.SystemTextJson.Benchmarks/paket.references b/benchmarks/FSharp.SystemTextJson.Benchmarks/paket.references index b2da4c2..56cdfa6 100644 --- a/benchmarks/FSharp.SystemTextJson.Benchmarks/paket.references +++ b/benchmarks/FSharp.SystemTextJson.Benchmarks/paket.references @@ -1,3 +1,4 @@ FSharp.Core BenchmarkDotNet -Newtonsoft.Json \ No newline at end of file +Newtonsoft.Json +BenchmarkDotNet.Diagnostics.Windows \ No newline at end of file diff --git a/paket.dependencies b/paket.dependencies index d78ec4c..53974bc 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -2,6 +2,7 @@ frameworks netstandard2.0, netcoreapp3.0 storage none source https://api.nuget.org/v3/index.json nuget FsCheck.XUnit +nuget BenchmarkDotNet.Diagnostics.Windows nuget FSharp.Core ~> 4.7.0 nuget Microsoft.NET.Test.Sdk ~> 16.3.0 nuget Microsoft.SourceLink.GitHub prerelease copy_local:true diff --git a/paket.lock b/paket.lock index e1035ac..e48268f 100644 --- a/paket.lock +++ b/paket.lock @@ -20,6 +20,9 @@ NUGET System.Xml.XmlSerializer (>= 4.3) System.Xml.XPath.XmlDocument (>= 4.3) BenchmarkDotNet.Annotations (0.11.5) + BenchmarkDotNet.Diagnostics.Windows (0.11.5) + BenchmarkDotNet (>= 0.11.5) + Microsoft.Diagnostics.Tracing.TraceEvent (>= 2.0.34) CommandLineParser (2.6) FsCheck (2.14) FSharp.Core (>= 4.2.3) From eae6076557d8eaec5d6c4a6c340933521efb523a Mon Sep 17 00:00:00 2001 From: Loic Denuziere Date: Sat, 10 Aug 2019 16:24:43 +0200 Subject: [PATCH 2/8] Add deserialize benchmark --- .../FSharp.SystemTextJson.Benchmarks/Program.fs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/benchmarks/FSharp.SystemTextJson.Benchmarks/Program.fs b/benchmarks/FSharp.SystemTextJson.Benchmarks/Program.fs index d1db159..c0aa9c1 100644 --- a/benchmarks/FSharp.SystemTextJson.Benchmarks/Program.fs +++ b/benchmarks/FSharp.SystemTextJson.Benchmarks/Program.fs @@ -38,16 +38,25 @@ type ArrayTestBase<'t>(instance: 't) = member val ArrayLength = 0 with get, set member val InstanceArray = [||] with get, set + + member val Serialized = "" with get, set [] member this.InitArray () = this.InstanceArray <- Array.replicate this.ArrayLength instance + this.Serialized <- this.InstanceArray |> JsonConvert.SerializeObject + + [] + member this.Serialize_Newtonsoft () = JsonConvert.SerializeObject this.InstanceArray + + [] + member this.Serialize_SystemTextJson () = System.Text.Json.JsonSerializer.Serialize(this.InstanceArray, systemTextOptions) [] - member this.Newtonsoft () = JsonConvert.SerializeObject this.InstanceArray + member this.Deserialize_Newtonsoft () = JsonConvert.DeserializeObject<'t[]> this.Serialized [] - member this.SystemTextJson () = System.Text.Json.JsonSerializer.Serialize(this.InstanceArray, systemTextOptions) + member this.Deserialize_SystemTextJson () = System.Text.Json.JsonSerializer.Deserialize<'t[]>(this.Serialized, systemTextOptions) let recordInstance = { name = "sample" From bb028ca930404ce4c04034beb9fe5f68da2c8dec Mon Sep 17 00:00:00 2001 From: Loic Denuziere Date: Sat, 10 Aug 2019 16:47:24 +0200 Subject: [PATCH 3/8] Implement record serialization with Reflection.Emit --- .../Program.fs | 2 +- .../FSharp.SystemTextJson.fsproj | 1 + src/FSharp.SystemTextJson/Record.fs | 30 ++------- src/FSharp.SystemTextJson/RecordField.fs | 67 +++++++++++++++++++ src/FSharp.SystemTextJson/paket.references | 4 +- 5 files changed, 77 insertions(+), 27 deletions(-) create mode 100644 src/FSharp.SystemTextJson/RecordField.fs diff --git a/benchmarks/FSharp.SystemTextJson.Benchmarks/Program.fs b/benchmarks/FSharp.SystemTextJson.Benchmarks/Program.fs index c0aa9c1..356632b 100644 --- a/benchmarks/FSharp.SystemTextJson.Benchmarks/Program.fs +++ b/benchmarks/FSharp.SystemTextJson.Benchmarks/Program.fs @@ -34,7 +34,7 @@ type ArrayTestBase<'t>(instance: 't) = options - [] + [] member val ArrayLength = 0 with get, set member val InstanceArray = [||] with get, set diff --git a/src/FSharp.SystemTextJson/FSharp.SystemTextJson.fsproj b/src/FSharp.SystemTextJson/FSharp.SystemTextJson.fsproj index 9ef1cab..a48f64e 100644 --- a/src/FSharp.SystemTextJson/FSharp.SystemTextJson.fsproj +++ b/src/FSharp.SystemTextJson/FSharp.SystemTextJson.fsproj @@ -8,6 +8,7 @@ + diff --git a/src/FSharp.SystemTextJson/Record.fs b/src/FSharp.SystemTextJson/Record.fs index 81ad8fc..0943948 100644 --- a/src/FSharp.SystemTextJson/Record.fs +++ b/src/FSharp.SystemTextJson/Record.fs @@ -1,32 +1,14 @@ namespace System.Text.Json.Serialization open System +open System.Runtime.Serialization open System.Text.Json open FSharp.Reflection -type internal RecordProperty = - { - Name: string - Type: Type - Ignore: bool - } - type JsonRecordConverter<'T>() = inherit JsonConverter<'T>() - static let fieldProps = - FSharpType.GetRecordFields(typeof<'T>, true) - |> Array.map (fun p -> - let name = - match p.GetCustomAttributes(typeof, true) with - | [| :? JsonPropertyNameAttribute as name |] -> name.Name - | _ -> p.Name - let ignore = - p.GetCustomAttributes(typeof, true) - |> Array.isEmpty - |> not - { Name = name; Type = p.PropertyType; Ignore = ignore } - ) + static let fieldProps = RecordField<'T>.properties() static let expectedFieldCount = fieldProps @@ -35,8 +17,6 @@ type JsonRecordConverter<'T>() = static let ctor = FSharpValue.PreComputeRecordConstructor(typeof<'T>, true) - static let dector = FSharpValue.PreComputeRecordReader(typeof<'T>, true) - static let fieldIndex (reader: byref) = let mutable found = ValueNone let mutable i = 0 @@ -74,11 +54,11 @@ type JsonRecordConverter<'T>() = override __.Write(writer, value, options) = writer.WriteStartObject() - (fieldProps, dector value) - ||> Array.iter2 (fun p v -> + fieldProps + |> Array.iter (fun p -> if not p.Ignore then writer.WritePropertyName(p.Name) - JsonSerializer.Serialize(writer, v, options)) + p.Serialize.Invoke(writer, value, options)) writer.WriteEndObject() type JsonRecordConverter() = diff --git a/src/FSharp.SystemTextJson/RecordField.fs b/src/FSharp.SystemTextJson/RecordField.fs new file mode 100644 index 0000000..e8d4238 --- /dev/null +++ b/src/FSharp.SystemTextJson/RecordField.fs @@ -0,0 +1,67 @@ +namespace System.Text.Json.Serialization + +open System +open System.Reflection +open FSharp.Reflection +open System.Reflection.Emit +open System.Text.Json + +type internal Serializer<'Record> = delegate of Utf8JsonWriter * 'Record * JsonSerializerOptions -> unit +type internal FieldReader<'Record, 'Field> = delegate of 'Record -> 'Field + +type internal RecordField<'Record> = + { + Name: string + Type: Type + Ignore: bool + Serialize: Serializer<'Record> + } + + static member name (p: PropertyInfo) = + match p.GetCustomAttributes(typeof, true) with + | [| :? JsonPropertyNameAttribute as name |] -> name.Name + | _ -> p.Name + + static member isIgnore (p: PropertyInfo) = + p.GetCustomAttributes(typeof, true) + |> Array.isEmpty + |> not + + static member serializer<'Field> (f: FieldInfo) = + let getter = + let dynMethod = + new DynamicMethod( + f.Name, + f.FieldType, + [| typeof |], + typedefof>.Module, + skipVisibility = true + ) + let gen = dynMethod.GetILGenerator() + gen.Emit(OpCodes.Ldarg_0) + gen.Emit(OpCodes.Ldfld, f) + gen.Emit(OpCodes.Ret) + dynMethod.CreateDelegate(typeof>) :?> FieldReader<'Record, 'Field> + Serializer<'Record>(fun writer r options -> + let v = getter.Invoke(r) + JsonSerializer.Serialize<'Field>(writer, v, options) + ) + + static member properties () = + let recordTy = typeof<'Record> + let fields = recordTy.GetFields(BindingFlags.Instance ||| BindingFlags.NonPublic) + let props = FSharpType.GetRecordFields(recordTy, true) + (fields, props) + ||> Array.map2 (fun f p -> + let serializer = + typeof>.GetMethod("serializer", BindingFlags.Static ||| BindingFlags.NonPublic) + .MakeGenericMethod(p.PropertyType) + .Invoke(null, [|f|]) + :?> Serializer<'Record> + { + Name = RecordField<'Record>.name p + Type = p.PropertyType + Ignore = RecordField<'Record>.isIgnore p + Serialize = serializer + } : RecordField<'Record> + ) diff --git a/src/FSharp.SystemTextJson/paket.references b/src/FSharp.SystemTextJson/paket.references index f6cb595..79265da 100644 --- a/src/FSharp.SystemTextJson/paket.references +++ b/src/FSharp.SystemTextJson/paket.references @@ -1,3 +1,5 @@ FSharp.Core System.Text.Json framework: = netstandard2.0 -Microsoft.SourceLink.GitHub \ No newline at end of file +System.Reflection.Emit.Lightweight framework: = netstandard2.0 +System.Reflection.Emit.ILGeneration framework: = netstandard2.0 +Microsoft.SourceLink.GitHub From 4ba370ac4696241eac433e0bb8f76febfc20dd6b Mon Sep 17 00:00:00 2001 From: Loic Denuziere Date: Sat, 10 Aug 2019 18:01:31 +0200 Subject: [PATCH 4/8] Implement struct record serialization with Reflection.Emit --- .../Program.fs | 18 ++++++++++++++++-- src/FSharp.SystemTextJson/Record.fs | 7 +++---- src/FSharp.SystemTextJson/RecordField.fs | 15 ++++++++------- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/benchmarks/FSharp.SystemTextJson.Benchmarks/Program.fs b/benchmarks/FSharp.SystemTextJson.Benchmarks/Program.fs index 356632b..6d97502 100644 --- a/benchmarks/FSharp.SystemTextJson.Benchmarks/Program.fs +++ b/benchmarks/FSharp.SystemTextJson.Benchmarks/Program.fs @@ -22,6 +22,12 @@ type TestRecord = thing: bool option time: System.DateTimeOffset } +[] +type TestStructRecord = + { name: string + thing: bool voption + time: System.DateTimeOffset } + type SimpleClass() = member val Name: string = null with get, set member val Thing: bool option = None with get, set @@ -58,7 +64,7 @@ type ArrayTestBase<'t>(instance: 't) = [] member this.Deserialize_SystemTextJson () = System.Text.Json.JsonSerializer.Deserialize<'t[]>(this.Serialized, systemTextOptions) -let recordInstance = +let recordInstance : TestRecord = { name = "sample" thing = Some true time = System.DateTimeOffset.UnixEpoch.AddDays(200.) } @@ -67,6 +73,14 @@ let recordInstance = type Records () = inherit ArrayTestBase(recordInstance) +let recordStructInstance : TestStructRecord = + { name = "sample" + thing = ValueSome true + time = System.DateTimeOffset.UnixEpoch.AddDays(200.) } + +type StructRecords () = + inherit ArrayTestBase(recordStructInstance) + type Classes() = inherit ArrayTestBase(SimpleClass(Name = "sample", Thing = Some true, Time = DateTimeOffset.UnixEpoch.AddDays(200.))) @@ -106,7 +120,7 @@ let config = .With(ExecutionValidator.FailOnError) let defaultSwitch () = - BenchmarkSwitcher([| typeof; typeof; typeof |]) + BenchmarkSwitcher([| typeof; typeof; typeof; typeof |]) [] diff --git a/src/FSharp.SystemTextJson/Record.fs b/src/FSharp.SystemTextJson/Record.fs index 0943948..2a431ea 100644 --- a/src/FSharp.SystemTextJson/Record.fs +++ b/src/FSharp.SystemTextJson/Record.fs @@ -8,7 +8,7 @@ open FSharp.Reflection type JsonRecordConverter<'T>() = inherit JsonConverter<'T>() - static let fieldProps = RecordField<'T>.properties() + static let fieldProps = RecordField<'T>.fields() static let expectedFieldCount = fieldProps @@ -54,11 +54,10 @@ type JsonRecordConverter<'T>() = override __.Write(writer, value, options) = writer.WriteStartObject() - fieldProps - |> Array.iter (fun p -> + for p in fieldProps do if not p.Ignore then writer.WritePropertyName(p.Name) - p.Serialize.Invoke(writer, value, options)) + p.Serialize.Invoke(writer, value, options) writer.WriteEndObject() type JsonRecordConverter() = diff --git a/src/FSharp.SystemTextJson/RecordField.fs b/src/FSharp.SystemTextJson/RecordField.fs index e8d4238..292a841 100644 --- a/src/FSharp.SystemTextJson/RecordField.fs +++ b/src/FSharp.SystemTextJson/RecordField.fs @@ -6,15 +6,14 @@ open FSharp.Reflection open System.Reflection.Emit open System.Text.Json -type internal Serializer<'Record> = delegate of Utf8JsonWriter * 'Record * JsonSerializerOptions -> unit -type internal FieldReader<'Record, 'Field> = delegate of 'Record -> 'Field +type internal Serializer = Action type internal RecordField<'Record> = { Name: string Type: Type Ignore: bool - Serialize: Serializer<'Record> + Serialize: Serializer } static member name (p: PropertyInfo) = @@ -39,15 +38,17 @@ type internal RecordField<'Record> = ) let gen = dynMethod.GetILGenerator() gen.Emit(OpCodes.Ldarg_0) + if f.DeclaringType.IsValueType then + gen.Emit(OpCodes.Unbox, typeof<'Record>) gen.Emit(OpCodes.Ldfld, f) gen.Emit(OpCodes.Ret) - dynMethod.CreateDelegate(typeof>) :?> FieldReader<'Record, 'Field> - Serializer<'Record>(fun writer r options -> + dynMethod.CreateDelegate(typeof>) :?> Func + Serializer(fun writer r options -> let v = getter.Invoke(r) JsonSerializer.Serialize<'Field>(writer, v, options) ) - static member properties () = + static member fields () = let recordTy = typeof<'Record> let fields = recordTy.GetFields(BindingFlags.Instance ||| BindingFlags.NonPublic) let props = FSharpType.GetRecordFields(recordTy, true) @@ -57,7 +58,7 @@ type internal RecordField<'Record> = typeof>.GetMethod("serializer", BindingFlags.Static ||| BindingFlags.NonPublic) .MakeGenericMethod(p.PropertyType) .Invoke(null, [|f|]) - :?> Serializer<'Record> + :?> Serializer { Name = RecordField<'Record>.name p Type = p.PropertyType From 059dde33eae93a17e522c91518fea3a36ae0c49d Mon Sep 17 00:00:00 2001 From: Loic Denuziere Date: Sun, 11 Aug 2019 14:37:29 +0200 Subject: [PATCH 5/8] Pass arguments to build -t benchmark --- build.fsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/build.fsx b/build.fsx index 28ab5d7..367e050 100644 --- a/build.fsx +++ b/build.fsx @@ -73,13 +73,19 @@ Target.create "Test" (fun _ -> /// This target doesn't need a dependency chain, because the benchmarks actually wrap and build the referenced /// project(s) as part of the run. -Target.create "Benchmark" (fun _ -> - DotNet.exec (fun o -> { o with - WorkingDirectory = Paths.benchmarks } ) "run" "-c release --filter \"*\"" +Target.create "Benchmark" (fun p -> + let args = p.Context.Arguments + seq { + yield! ["-p"; Paths.benchmarks; "-c"; "release"; "--"] + if not (List.contains "-f" args || List.contains "--filter" args) then + yield! ["--filter"; "*"] + yield! args + } + |> Args.toWindowsCommandLine + |> DotNet.exec id "run" |> fun r -> - if r.OK - then () - else failwithf "Benchmarks failed with code %d:\n%A" r.ExitCode r.Errors + if not r.OK then + failwithf "Benchmarks failed with code %d:\n%A" r.ExitCode r.Errors ) Target.create "All" ignore From 7176590576eb4eb4ab5824f20a4b476d73b6df31 Mon Sep 17 00:00:00 2001 From: Loic Denuziere Date: Sun, 11 Aug 2019 14:40:22 +0200 Subject: [PATCH 6/8] Implement record deserialization with Reflection.Emit --- .../FSharp.SystemTextJson.fsproj | 2 +- .../Record.Reflection.fs | 102 ++++++++++++++++++ src/FSharp.SystemTextJson/Record.fs | 25 +++-- src/FSharp.SystemTextJson/RecordField.fs | 68 ------------ .../Test.Record.fs | 8 +- 5 files changed, 121 insertions(+), 84 deletions(-) create mode 100644 src/FSharp.SystemTextJson/Record.Reflection.fs delete mode 100644 src/FSharp.SystemTextJson/RecordField.fs diff --git a/src/FSharp.SystemTextJson/FSharp.SystemTextJson.fsproj b/src/FSharp.SystemTextJson/FSharp.SystemTextJson.fsproj index a48f64e..86a900f 100644 --- a/src/FSharp.SystemTextJson/FSharp.SystemTextJson.fsproj +++ b/src/FSharp.SystemTextJson/FSharp.SystemTextJson.fsproj @@ -8,7 +8,7 @@ - + diff --git a/src/FSharp.SystemTextJson/Record.Reflection.fs b/src/FSharp.SystemTextJson/Record.Reflection.fs new file mode 100644 index 0000000..c589be6 --- /dev/null +++ b/src/FSharp.SystemTextJson/Record.Reflection.fs @@ -0,0 +1,102 @@ +namespace System.Text.Json.Serialization + +open System +open System.Reflection +open FSharp.Reflection +open System.Reflection.Emit +open System.Text.Json + +type internal Serializer = delegate of Utf8JsonWriter * obj * JsonSerializerOptions -> unit +type internal Deserializer = delegate of byref * obj * JsonSerializerOptions -> unit + +type internal RecordField<'Record> = + { + Name: string + Type: Type + Ignore: bool + Serialize: Serializer + Deserialize: Deserializer + } + +module internal RecordReflection = + + let private name (p: PropertyInfo) = + match p.GetCustomAttributes(typeof, true) with + | [| :? JsonPropertyNameAttribute as name |] -> name.Name + | _ -> p.Name + + let private isIgnore (p: PropertyInfo) = + p.GetCustomAttributes(typeof, true) + |> Array.isEmpty + |> not + + let private deserializer<'Field> (f: FieldInfo) = + let setter = + let dynMethod = + new DynamicMethod( + f.Name, + typeof, + [| typeof; f.FieldType |], + typedefof>.Module, + skipVisibility = true + ) + let gen = dynMethod.GetILGenerator() + gen.Emit(OpCodes.Ldarg_0) + if f.DeclaringType.IsValueType then + gen.Emit(OpCodes.Unbox, f.DeclaringType) + gen.Emit(OpCodes.Ldarg_1) + gen.Emit(OpCodes.Stfld, f) + gen.Emit(OpCodes.Ret) + dynMethod.CreateDelegate(typeof>) :?> Action + Deserializer(fun reader record options -> + let value = JsonSerializer.Deserialize<'Field>(&reader, options) + setter.Invoke(record, value)) + + let private serializer<'Field> (f: FieldInfo) = + let getter = + let dynMethod = + new DynamicMethod( + f.Name, + f.FieldType, + [| typeof |], + typedefof>.Module, + skipVisibility = true + ) + let gen = dynMethod.GetILGenerator() + gen.Emit(OpCodes.Ldarg_0) + if f.DeclaringType.IsValueType then + gen.Emit(OpCodes.Unbox, f.DeclaringType) + gen.Emit(OpCodes.Ldfld, f) + gen.Emit(OpCodes.Ret) + dynMethod.CreateDelegate(typeof>) :?> Func + Serializer(fun writer record options -> + let v = getter.Invoke(record) + JsonSerializer.Serialize<'Field>(writer, v, options) + ) + + let private thisModule = typedefof>.Assembly.GetType("System.Text.Json.Serialization.RecordReflection") + + let fields<'Record> () = + let recordTy = typeof<'Record> + let fields = recordTy.GetFields(BindingFlags.Instance ||| BindingFlags.NonPublic) + let props = FSharpType.GetRecordFields(recordTy, true) + (fields, props) + ||> Array.map2 (fun f p -> + let serializer = + thisModule.GetMethod("serializer", BindingFlags.Static ||| BindingFlags.NonPublic) + .MakeGenericMethod(p.PropertyType) + .Invoke(null, [|f|]) + :?> Serializer + let deserializer = + thisModule.GetMethod("deserializer", BindingFlags.Static ||| BindingFlags.NonPublic) + .MakeGenericMethod(p.PropertyType) + .Invoke(null, [|f|]) + :?> Deserializer + { + Name = name p + Type = p.PropertyType + Ignore = isIgnore p + Serialize = serializer + Deserialize = deserializer + } : RecordField<'Record> + ) diff --git a/src/FSharp.SystemTextJson/Record.fs b/src/FSharp.SystemTextJson/Record.fs index 2a431ea..ed62cd2 100644 --- a/src/FSharp.SystemTextJson/Record.fs +++ b/src/FSharp.SystemTextJson/Record.fs @@ -8,22 +8,25 @@ open FSharp.Reflection type JsonRecordConverter<'T>() = inherit JsonConverter<'T>() - static let fieldProps = RecordField<'T>.fields() + static let ty = typeof<'T> + + static let fields = RecordReflection.fields<'T>() static let expectedFieldCount = - fieldProps + fields |> Seq.filter (fun p -> not p.Ignore) |> Seq.length - static let ctor = FSharpValue.PreComputeRecordConstructor(typeof<'T>, true) + static let ctor() = + FormatterServices.GetUninitializedObject(ty) static let fieldIndex (reader: byref) = let mutable found = ValueNone let mutable i = 0 - while found.IsNone && i < fieldProps.Length do - let p = fieldProps.[i] + while found.IsNone && i < fields.Length do + let p = fields.[i] if reader.ValueTextEquals(p.Name.AsSpan()) then - found <- ValueSome (struct (i, p)) + found <- ValueSome p else i <- i + 1 found @@ -32,7 +35,7 @@ type JsonRecordConverter<'T>() = if reader.TokenType <> JsonTokenType.StartObject then raise (JsonException("Failed to parse record type " + typeToConvert.FullName + ", expected JSON object, found " + string reader.TokenType)) - let fields = Array.zeroCreate fieldProps.Length + let res = ctor() let mutable cont = true let mutable fieldsFound = 0 while cont && reader.Read() do @@ -41,20 +44,20 @@ type JsonRecordConverter<'T>() = cont <- false | JsonTokenType.PropertyName -> match fieldIndex &reader with - | ValueSome (i, p) when not p.Ignore -> + | ValueSome p when not p.Ignore -> fieldsFound <- fieldsFound + 1 - fields.[i] <- JsonSerializer.Deserialize(&reader, p.Type, options) + p.Deserialize.Invoke(&reader, res, options) | _ -> reader.Skip() | _ -> () if fieldsFound < expectedFieldCount then raise (JsonException("Missing field for record type " + typeToConvert.FullName)) - ctor fields :?> 'T + res :?> 'T override __.Write(writer, value, options) = writer.WriteStartObject() - for p in fieldProps do + for p in fields do if not p.Ignore then writer.WritePropertyName(p.Name) p.Serialize.Invoke(writer, value, options) diff --git a/src/FSharp.SystemTextJson/RecordField.fs b/src/FSharp.SystemTextJson/RecordField.fs deleted file mode 100644 index 292a841..0000000 --- a/src/FSharp.SystemTextJson/RecordField.fs +++ /dev/null @@ -1,68 +0,0 @@ -namespace System.Text.Json.Serialization - -open System -open System.Reflection -open FSharp.Reflection -open System.Reflection.Emit -open System.Text.Json - -type internal Serializer = Action - -type internal RecordField<'Record> = - { - Name: string - Type: Type - Ignore: bool - Serialize: Serializer - } - - static member name (p: PropertyInfo) = - match p.GetCustomAttributes(typeof, true) with - | [| :? JsonPropertyNameAttribute as name |] -> name.Name - | _ -> p.Name - - static member isIgnore (p: PropertyInfo) = - p.GetCustomAttributes(typeof, true) - |> Array.isEmpty - |> not - - static member serializer<'Field> (f: FieldInfo) = - let getter = - let dynMethod = - new DynamicMethod( - f.Name, - f.FieldType, - [| typeof |], - typedefof>.Module, - skipVisibility = true - ) - let gen = dynMethod.GetILGenerator() - gen.Emit(OpCodes.Ldarg_0) - if f.DeclaringType.IsValueType then - gen.Emit(OpCodes.Unbox, typeof<'Record>) - gen.Emit(OpCodes.Ldfld, f) - gen.Emit(OpCodes.Ret) - dynMethod.CreateDelegate(typeof>) :?> Func - Serializer(fun writer r options -> - let v = getter.Invoke(r) - JsonSerializer.Serialize<'Field>(writer, v, options) - ) - - static member fields () = - let recordTy = typeof<'Record> - let fields = recordTy.GetFields(BindingFlags.Instance ||| BindingFlags.NonPublic) - let props = FSharpType.GetRecordFields(recordTy, true) - (fields, props) - ||> Array.map2 (fun f p -> - let serializer = - typeof>.GetMethod("serializer", BindingFlags.Static ||| BindingFlags.NonPublic) - .MakeGenericMethod(p.PropertyType) - .Invoke(null, [|f|]) - :?> Serializer - { - Name = RecordField<'Record>.name p - Type = p.PropertyType - Ignore = RecordField<'Record>.isIgnore p - Serialize = serializer - } : RecordField<'Record> - ) diff --git a/tests/FSharp.SystemTextJson.Tests/Test.Record.fs b/tests/FSharp.SystemTextJson.Tests/Test.Record.fs index 30dc7ad..a8ab74b 100644 --- a/tests/FSharp.SystemTextJson.Tests/Test.Record.fs +++ b/tests/FSharp.SystemTextJson.Tests/Test.Record.fs @@ -25,7 +25,7 @@ module NonStruct = type B = { - bx: int + bx: uint32 by: string } @@ -35,11 +35,11 @@ module NonStruct = [] let ``deserialize via options`` () = let actual = JsonSerializer.Deserialize("""{"bx":1,"by":"b"}""", options) - Assert.Equal({bx=1;by="b"}, actual) + Assert.Equal({bx=1u;by="b"}, actual) [] let ``serialize via options`` () = - let actual = JsonSerializer.Serialize({bx=1;by="b"}, options) + let actual = JsonSerializer.Serialize({bx=1u;by="b"}, options) Assert.Equal("""{"bx":1,"by":"b"}""", actual) type C = @@ -50,7 +50,7 @@ module NonStruct = [] let ``deserialize nested`` () = let actual = JsonSerializer.Deserialize("""{"cx":{"bx":1,"by":"b"}}""", options) - Assert.Equal({cx={bx=1;by="b"}}, actual) + Assert.Equal({cx={bx=1u;by="b"}}, actual) [] let ``deserialize anonymous`` () = From 5572c0909a96429aaa94fc2c7e89c39fe227e94d Mon Sep 17 00:00:00 2001 From: Loic Denuziere Date: Sun, 11 Aug 2019 18:00:37 +0200 Subject: [PATCH 7/8] Optimize struct record deserialization to not box --- .../Record.Reflection.fs | 48 ++++++++++++++----- src/FSharp.SystemTextJson/Record.fs | 20 ++++++-- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/FSharp.SystemTextJson/Record.Reflection.fs b/src/FSharp.SystemTextJson/Record.Reflection.fs index c589be6..f2d1b67 100644 --- a/src/FSharp.SystemTextJson/Record.Reflection.fs +++ b/src/FSharp.SystemTextJson/Record.Reflection.fs @@ -6,8 +6,18 @@ open FSharp.Reflection open System.Reflection.Emit open System.Text.Json -type internal Serializer = delegate of Utf8JsonWriter * obj * JsonSerializerOptions -> unit -type internal Deserializer = delegate of byref * obj * JsonSerializerOptions -> unit +type internal Serializer = Action + +type internal RefobjFieldSetter<'Record, 'Field> = Action<'Record, 'Field> +type internal StructFieldSetter<'Record, 'Field> = delegate of byref<'Record> * 'Field -> unit + +type internal RefobjDeserializer<'Record> = delegate of byref * 'Record * JsonSerializerOptions -> unit +type internal StructDeserializer<'Record> = delegate of byref * byref<'Record> * JsonSerializerOptions -> unit + +[] +type internal Deserializer<'Record> = + | DStruct of s: StructDeserializer<'Record> + | DRefobj of n: RefobjDeserializer<'Record> type internal RecordField<'Record> = { @@ -15,7 +25,7 @@ type internal RecordField<'Record> = Type: Type Ignore: bool Serialize: Serializer - Deserialize: Deserializer + Deserialize: Deserializer<'Record> } module internal RecordReflection = @@ -30,27 +40,39 @@ module internal RecordReflection = |> Array.isEmpty |> not - let private deserializer<'Field> (f: FieldInfo) = + let private deserializer<'Record, 'Field> (f: FieldInfo) = let setter = let dynMethod = new DynamicMethod( f.Name, typeof, - [| typeof; f.FieldType |], + [| + (if f.DeclaringType.IsValueType + then typeof<'Record>.MakeByRefType() + else typeof<'Record>) + f.FieldType + |], typedefof>.Module, skipVisibility = true ) let gen = dynMethod.GetILGenerator() gen.Emit(OpCodes.Ldarg_0) - if f.DeclaringType.IsValueType then - gen.Emit(OpCodes.Unbox, f.DeclaringType) gen.Emit(OpCodes.Ldarg_1) gen.Emit(OpCodes.Stfld, f) gen.Emit(OpCodes.Ret) - dynMethod.CreateDelegate(typeof>) :?> Action - Deserializer(fun reader record options -> - let value = JsonSerializer.Deserialize<'Field>(&reader, options) - setter.Invoke(record, value)) + dynMethod + if f.DeclaringType.IsValueType then + let setter = setter.CreateDelegate(typeof>) :?> StructFieldSetter<'Record, 'Field> + StructDeserializer<'Record>(fun reader record options -> + let value = JsonSerializer.Deserialize<'Field>(&reader, options) + setter.Invoke(&record, value)) + |> DStruct + else + let setter = setter.CreateDelegate(typeof>) :?> RefobjFieldSetter<'Record, 'Field> + RefobjDeserializer<'Record>(fun reader record options -> + let value = JsonSerializer.Deserialize<'Field>(&reader, options) + setter.Invoke(record, value)) + |> DRefobj let private serializer<'Field> (f: FieldInfo) = let getter = @@ -89,9 +111,9 @@ module internal RecordReflection = :?> Serializer let deserializer = thisModule.GetMethod("deserializer", BindingFlags.Static ||| BindingFlags.NonPublic) - .MakeGenericMethod(p.PropertyType) + .MakeGenericMethod(recordTy, p.PropertyType) .Invoke(null, [|f|]) - :?> Deserializer + :?> Deserializer<'Record> { Name = name p Type = p.PropertyType diff --git a/src/FSharp.SystemTextJson/Record.fs b/src/FSharp.SystemTextJson/Record.fs index ed62cd2..b6413ce 100644 --- a/src/FSharp.SystemTextJson/Record.fs +++ b/src/FSharp.SystemTextJson/Record.fs @@ -4,6 +4,7 @@ open System open System.Runtime.Serialization open System.Text.Json open FSharp.Reflection +open System.Collections.Generic type JsonRecordConverter<'T>() = inherit JsonConverter<'T>() @@ -12,13 +13,20 @@ type JsonRecordConverter<'T>() = static let fields = RecordReflection.fields<'T>() + static let fieldIndices = Dictionary(StringComparer.InvariantCulture) + static do fields |> Array.iteri (fun i f -> + fieldIndices.[f.Name] <- f) + static let expectedFieldCount = fields |> Seq.filter (fun p -> not p.Ignore) |> Seq.length - static let ctor() = - FormatterServices.GetUninitializedObject(ty) + static let ctor = + if ty.IsValueType then + fun () -> Unchecked.defaultof<'T> + else + fun () -> FormatterServices.GetUninitializedObject(ty) :?> 'T static let fieldIndex (reader: byref) = let mutable found = ValueNone @@ -35,7 +43,7 @@ type JsonRecordConverter<'T>() = if reader.TokenType <> JsonTokenType.StartObject then raise (JsonException("Failed to parse record type " + typeToConvert.FullName + ", expected JSON object, found " + string reader.TokenType)) - let res = ctor() + let mutable res = ctor() let mutable cont = true let mutable fieldsFound = 0 while cont && reader.Read() do @@ -46,14 +54,16 @@ type JsonRecordConverter<'T>() = match fieldIndex &reader with | ValueSome p when not p.Ignore -> fieldsFound <- fieldsFound + 1 - p.Deserialize.Invoke(&reader, res, options) + match p.Deserialize with + | DStruct p -> p.Invoke(&reader, &res, options) + | DRefobj p -> p.Invoke(&reader, res, options) | _ -> reader.Skip() | _ -> () if fieldsFound < expectedFieldCount then raise (JsonException("Missing field for record type " + typeToConvert.FullName)) - res :?> 'T + res override __.Write(writer, value, options) = writer.WriteStartObject() From bf52a11032e14b8302c231842688a1c4334bf0f8 Mon Sep 17 00:00:00 2001 From: Loic Denuziere Date: Sun, 11 Aug 2019 18:21:26 +0200 Subject: [PATCH 8/8] Optimize struct record serialization to not box --- .../Record.Reflection.fs | 47 ++++++++++++++----- src/FSharp.SystemTextJson/Record.fs | 4 +- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/FSharp.SystemTextJson/Record.Reflection.fs b/src/FSharp.SystemTextJson/Record.Reflection.fs index f2d1b67..0e05313 100644 --- a/src/FSharp.SystemTextJson/Record.Reflection.fs +++ b/src/FSharp.SystemTextJson/Record.Reflection.fs @@ -6,7 +6,16 @@ open FSharp.Reflection open System.Reflection.Emit open System.Text.Json -type internal Serializer = Action +type internal RefobjFieldGetter<'Record, 'Field> = Func<'Record, 'Field> +type internal StructFieldGetter<'Record, 'Field> = delegate of inref<'Record> -> 'Field + +type internal RefobjSerializer<'Record> = Action +type internal StructSerializer<'Record> = delegate of Utf8JsonWriter * inref<'Record> * JsonSerializerOptions -> unit + +[] +type internal Serializer<'Record> = + | SStruct of s: StructSerializer<'Record> + | SRefobj of n: RefobjSerializer<'Record> type internal RefobjFieldSetter<'Record, 'Field> = Action<'Record, 'Field> type internal StructFieldSetter<'Record, 'Field> = delegate of byref<'Record> * 'Field -> unit @@ -24,7 +33,7 @@ type internal RecordField<'Record> = Name: string Type: Type Ignore: bool - Serialize: Serializer + Serialize: Serializer<'Record> Deserialize: Deserializer<'Record> } @@ -74,27 +83,39 @@ module internal RecordReflection = setter.Invoke(record, value)) |> DRefobj - let private serializer<'Field> (f: FieldInfo) = + let private serializer<'Record, 'Field> (f: FieldInfo) = let getter = let dynMethod = new DynamicMethod( f.Name, f.FieldType, - [| typeof |], + [| + (if f.DeclaringType.IsValueType + then typeof<'Record>.MakeByRefType() + else typeof<'Record>) + |], typedefof>.Module, skipVisibility = true ) let gen = dynMethod.GetILGenerator() gen.Emit(OpCodes.Ldarg_0) - if f.DeclaringType.IsValueType then - gen.Emit(OpCodes.Unbox, f.DeclaringType) gen.Emit(OpCodes.Ldfld, f) gen.Emit(OpCodes.Ret) - dynMethod.CreateDelegate(typeof>) :?> Func - Serializer(fun writer record options -> - let v = getter.Invoke(record) - JsonSerializer.Serialize<'Field>(writer, v, options) - ) + dynMethod + if f.DeclaringType.IsValueType then + let getter = getter.CreateDelegate(typeof>) :?> StructFieldGetter<'Record, 'Field> + StructSerializer<'Record>(fun writer record options -> + let v = getter.Invoke(&record) + JsonSerializer.Serialize<'Field>(writer, v, options) + ) + |> SStruct + else + let getter = getter.CreateDelegate(typeof>) :?> RefobjFieldGetter<'Record, 'Field> + RefobjSerializer<'Record>(fun writer record options -> + let v = getter.Invoke(record) + JsonSerializer.Serialize<'Field>(writer, v, options) + ) + |> SRefobj let private thisModule = typedefof>.Assembly.GetType("System.Text.Json.Serialization.RecordReflection") @@ -106,9 +127,9 @@ module internal RecordReflection = ||> Array.map2 (fun f p -> let serializer = thisModule.GetMethod("serializer", BindingFlags.Static ||| BindingFlags.NonPublic) - .MakeGenericMethod(p.PropertyType) + .MakeGenericMethod(recordTy, p.PropertyType) .Invoke(null, [|f|]) - :?> Serializer + :?> Serializer<'Record> let deserializer = thisModule.GetMethod("deserializer", BindingFlags.Static ||| BindingFlags.NonPublic) .MakeGenericMethod(recordTy, p.PropertyType) diff --git a/src/FSharp.SystemTextJson/Record.fs b/src/FSharp.SystemTextJson/Record.fs index b6413ce..8284d2c 100644 --- a/src/FSharp.SystemTextJson/Record.fs +++ b/src/FSharp.SystemTextJson/Record.fs @@ -70,7 +70,9 @@ type JsonRecordConverter<'T>() = for p in fields do if not p.Ignore then writer.WritePropertyName(p.Name) - p.Serialize.Invoke(writer, value, options) + match p.Serialize with + | SStruct p -> p.Invoke(writer, &value, options) + | SRefobj p -> p.Invoke(writer, value, options) writer.WriteEndObject() type JsonRecordConverter() =