Skip to content

Commit

Permalink
Optimize struct record deserialization to not box
Browse files Browse the repository at this point in the history
  • Loading branch information
Tarmil committed Oct 4, 2019
1 parent 7176590 commit 5572c09
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 18 deletions.
48 changes: 35 additions & 13 deletions src/FSharp.SystemTextJson/Record.Reflection.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,26 @@ 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<Utf8JsonReader> * obj * JsonSerializerOptions -> unit
type internal Serializer = Action<Utf8JsonWriter, obj, JsonSerializerOptions>

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<Utf8JsonReader> * 'Record * JsonSerializerOptions -> unit
type internal StructDeserializer<'Record> = delegate of byref<Utf8JsonReader> * byref<'Record> * JsonSerializerOptions -> unit

[<Struct>]
type internal Deserializer<'Record> =
| DStruct of s: StructDeserializer<'Record>
| DRefobj of n: RefobjDeserializer<'Record>

type internal RecordField<'Record> =
{
Name: string
Type: Type
Ignore: bool
Serialize: Serializer
Deserialize: Deserializer
Deserialize: Deserializer<'Record>
}

module internal RecordReflection =
Expand All @@ -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<Void>,
[| typeof<obj>; f.FieldType |],
[|
(if f.DeclaringType.IsValueType
then typeof<'Record>.MakeByRefType()
else typeof<'Record>)
f.FieldType
|],
typedefof<RecordField<_>>.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<obj, 'Field>>) :?> Action<obj, 'Field>
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>>) :?> 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>>) :?> 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 =
Expand Down Expand Up @@ -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
Expand Down
20 changes: 15 additions & 5 deletions src/FSharp.SystemTextJson/Record.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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>()
Expand All @@ -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<Utf8JsonReader>) =
let mutable found = ValueNone
Expand All @@ -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
Expand All @@ -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()
Expand Down

0 comments on commit 5572c09

Please sign in to comment.