Skip to content

Commit 10c3f80

Browse files
committed
Optimize struct record deserialization to not box
1 parent 594502b commit 10c3f80

2 files changed

Lines changed: 50 additions & 18 deletions

File tree

src/FSharp.SystemTextJson/Record.Reflection.fs

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,26 @@ open FSharp.Reflection
66
open System.Reflection.Emit
77
open System.Text.Json
88

9-
type internal Serializer = delegate of Utf8JsonWriter * obj * JsonSerializerOptions -> unit
10-
type internal Deserializer = delegate of byref<Utf8JsonReader> * obj * JsonSerializerOptions -> unit
9+
type internal Serializer = Action<Utf8JsonWriter, obj, JsonSerializerOptions>
10+
11+
type internal RefobjFieldSetter<'Record, 'Field> = Action<'Record, 'Field>
12+
type internal StructFieldSetter<'Record, 'Field> = delegate of byref<'Record> * 'Field -> unit
13+
14+
type internal RefobjDeserializer<'Record> = delegate of byref<Utf8JsonReader> * 'Record * JsonSerializerOptions -> unit
15+
type internal StructDeserializer<'Record> = delegate of byref<Utf8JsonReader> * byref<'Record> * JsonSerializerOptions -> unit
16+
17+
[<Struct>]
18+
type internal Deserializer<'Record> =
19+
| DStruct of s: StructDeserializer<'Record>
20+
| DRefobj of n: RefobjDeserializer<'Record>
1121

1222
type internal RecordField<'Record> =
1323
{
1424
Name: string
1525
Type: Type
1626
Ignore: bool
1727
Serialize: Serializer
18-
Deserialize: Deserializer
28+
Deserialize: Deserializer<'Record>
1929
}
2030

2131
module internal RecordReflection =
@@ -30,27 +40,39 @@ module internal RecordReflection =
3040
|> Array.isEmpty
3141
|> not
3242

33-
let private deserializer<'Field> (f: FieldInfo) =
43+
let private deserializer<'Record, 'Field> (f: FieldInfo) =
3444
let setter =
3545
let dynMethod =
3646
new DynamicMethod(
3747
f.Name,
3848
typeof<Void>,
39-
[| typeof<obj>; f.FieldType |],
49+
[|
50+
(if f.DeclaringType.IsValueType
51+
then typeof<'Record>.MakeByRefType()
52+
else typeof<'Record>)
53+
f.FieldType
54+
|],
4055
typedefof<RecordField<_>>.Module,
4156
skipVisibility = true
4257
)
4358
let gen = dynMethod.GetILGenerator()
4459
gen.Emit(OpCodes.Ldarg_0)
45-
if f.DeclaringType.IsValueType then
46-
gen.Emit(OpCodes.Unbox, f.DeclaringType)
4760
gen.Emit(OpCodes.Ldarg_1)
4861
gen.Emit(OpCodes.Stfld, f)
4962
gen.Emit(OpCodes.Ret)
50-
dynMethod.CreateDelegate(typeof<Action<obj, 'Field>>) :?> Action<obj, 'Field>
51-
Deserializer(fun reader record options ->
52-
let value = JsonSerializer.Deserialize<'Field>(&reader, options)
53-
setter.Invoke(record, value))
63+
dynMethod
64+
if f.DeclaringType.IsValueType then
65+
let setter = setter.CreateDelegate(typeof<StructFieldSetter<'Record, 'Field>>) :?> StructFieldSetter<'Record, 'Field>
66+
StructDeserializer<'Record>(fun reader record options ->
67+
let value = JsonSerializer.Deserialize<'Field>(&reader, options)
68+
setter.Invoke(&record, value))
69+
|> DStruct
70+
else
71+
let setter = setter.CreateDelegate(typeof<RefobjFieldSetter<'Record, 'Field>>) :?> RefobjFieldSetter<'Record, 'Field>
72+
RefobjDeserializer<'Record>(fun reader record options ->
73+
let value = JsonSerializer.Deserialize<'Field>(&reader, options)
74+
setter.Invoke(record, value))
75+
|> DRefobj
5476

5577
let private serializer<'Field> (f: FieldInfo) =
5678
let getter =
@@ -89,9 +111,9 @@ module internal RecordReflection =
89111
:?> Serializer
90112
let deserializer =
91113
thisModule.GetMethod("deserializer", BindingFlags.Static ||| BindingFlags.NonPublic)
92-
.MakeGenericMethod(p.PropertyType)
114+
.MakeGenericMethod(recordTy, p.PropertyType)
93115
.Invoke(null, [|f|])
94-
:?> Deserializer
116+
:?> Deserializer<'Record>
95117
{
96118
Name = name p
97119
Type = p.PropertyType

src/FSharp.SystemTextJson/Record.fs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ open System
44
open System.Runtime.Serialization
55
open System.Text.Json
66
open FSharp.Reflection
7+
open System.Collections.Generic
78

89
type JsonRecordConverter<'T>() =
910
inherit JsonConverter<'T>()
@@ -12,13 +13,20 @@ type JsonRecordConverter<'T>() =
1213

1314
static let fields = RecordReflection.fields<'T>()
1415

16+
static let fieldIndices = Dictionary(StringComparer.InvariantCulture)
17+
static do fields |> Array.iteri (fun i f ->
18+
fieldIndices.[f.Name] <- f)
19+
1520
static let expectedFieldCount =
1621
fields
1722
|> Seq.filter (fun p -> not p.Ignore)
1823
|> Seq.length
1924

20-
static let ctor() =
21-
FormatterServices.GetUninitializedObject(ty)
25+
static let ctor =
26+
if ty.IsValueType then
27+
fun () -> Unchecked.defaultof<'T>
28+
else
29+
fun () -> FormatterServices.GetUninitializedObject(ty) :?> 'T
2230

2331
static let fieldIndex (reader: byref<Utf8JsonReader>) =
2432
let mutable found = ValueNone
@@ -35,7 +43,7 @@ type JsonRecordConverter<'T>() =
3543
if reader.TokenType <> JsonTokenType.StartObject then
3644
raise (JsonException("Failed to parse record type " + typeToConvert.FullName + ", expected JSON object, found " + string reader.TokenType))
3745

38-
let res = ctor()
46+
let mutable res = ctor()
3947
let mutable cont = true
4048
let mutable fieldsFound = 0
4149
while cont && reader.Read() do
@@ -46,14 +54,16 @@ type JsonRecordConverter<'T>() =
4654
match fieldIndex &reader with
4755
| ValueSome p when not p.Ignore ->
4856
fieldsFound <- fieldsFound + 1
49-
p.Deserialize.Invoke(&reader, res, options)
57+
match p.Deserialize with
58+
| DStruct p -> p.Invoke(&reader, &res, options)
59+
| DRefobj p -> p.Invoke(&reader, res, options)
5060
| _ ->
5161
reader.Skip()
5262
| _ -> ()
5363

5464
if fieldsFound < expectedFieldCount then
5565
raise (JsonException("Missing field for record type " + typeToConvert.FullName))
56-
res :?> 'T
66+
res
5767

5868
override __.Write(writer, value, options) =
5969
writer.WriteStartObject()

0 commit comments

Comments
 (0)