Skip to content

Commit bdb45d1

Browse files
committed
Trim JSON hot-path overhead
1 parent d50dfec commit bdb45d1

3 files changed

Lines changed: 28 additions & 19 deletions

File tree

benchmarks/CodecMapper.Benchmarks.Runner/Program.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ module Bench =
3939
/// Keep the batch deterministic so manual snapshots and BenchmarkDotNet
4040
/// runs are comparing the same wire shape and allocation profile.
4141
let people =
42-
[ 1..100 ]
42+
[ 1..50 ]
4343
|> List.map (fun id -> {
4444
Id = id
4545
Name = $"Benchmark User {id}"

benchmarks/CodecMapper.Benchmarks/Program.fs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ open BenchmarkDotNet.Toolchains.InProcess.Emit
1010
[<MemoryDiagnoser>]
1111
type CompetitiveBenchmarks() =
1212
///
13-
/// Benchmarking `100` records amortizes fixed serializer overhead and puts
13+
/// Benchmarking `50` records amortizes fixed serializer overhead and puts
1414
/// the comparisons closer to the payload sizes users actually send around.
1515
let people =
16-
[ 1..100 ]
16+
[ 1..50 ]
1717
|> List.map (fun id -> {
1818
Id = id
1919
Name = $"Benchmark User {id}"
@@ -27,7 +27,7 @@ type CompetitiveBenchmarks() =
2727

2828
let jsonBytes = System.Text.Encoding.UTF8.GetBytes(json)
2929

30-
// --- Batch of 100 records ---
30+
// --- Batch of 50 records ---
3131

3232
[<Benchmark(Baseline = true)>]
3333
member _.STJ_Json_Serialize() = StjBench.serialize people

src/CodecMapper/Json.fs

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ module Json =
2525
Decode: Decoder<'T>
2626
}
2727

28+
///
29+
/// Tiny payloads fit comfortably in the old `128`-byte buffer, but the
30+
/// benchmark runner now measures batches of `100` records where repeated
31+
/// growth and copy steps dominate allocation churn. Starting larger keeps
32+
/// the hot JSON path closer to realistic message sizes.
33+
let private defaultSerializeBufferCapacity = 4096
34+
2835
type internal DecodePathSegment =
2936
| Property of string
3037
| Index of int
@@ -852,13 +859,25 @@ module Json =
852859
fields
853860
|> Array.mapi (fun i f ->
854861
let codec = compileUntyped f.Schema
862+
let encodedName = "\"" + f.Name + "\":"
855863

856864
{|
857865
Name = f.Name
866+
EncodedName = encodedName
858867
Index = i
859868
Codec = codec
860869
|})
861870

871+
///
872+
/// Object decode used to linearly scan every field name for every
873+
/// property in the payload. A fixed lookup table keeps the compiled
874+
/// cost up front and removes repeated per-property scans.
875+
let fieldIndices = Dictionary<string, int>(compiledFields.Length)
876+
877+
do
878+
for field in compiledFields do
879+
fieldIndices[field.Name] <- field.Index
880+
862881
let encoder (writer: IByteWriter) (vObj: obj) =
863882
writer.WriteByte(123uy)
864883
let mutable first = true
@@ -867,10 +886,7 @@ module Json =
867886
if not first then
868887
writer.WriteByte(44uy)
869888

870-
writer.WriteByte(34uy)
871-
writer.WriteString(f.Name)
872-
writer.WriteByte(34uy)
873-
writer.WriteByte(58uy)
889+
writer.WriteString(f.EncodedName)
874890
f.Codec.Encode writer (fields[f.Index].GetValue vObj)
875891
first <- false
876892

@@ -900,17 +916,10 @@ module Json =
900916
failwith "Expected :"
901917

902918
let valSrc = Runtime.skipWhitespace (afterColon.Advance(1))
903-
let mutable found = false
904-
let mutable i = 0
905-
906-
while i < compiledFields.Length && not found do
907-
let f = compiledFields[i]
908919

909-
if f.Name = key then
910-
fieldSources[f.Index] <- valSrc
911-
found <- true
912-
else
913-
i <- i + 1
920+
match fieldIndices.TryGetValue(key) with
921+
| true, index -> fieldSources[index] <- valSrc
922+
| false, _ -> ()
914923

915924
let afterVal = Runtime.skipWhitespace (Runtime.skipValue valSrc)
916925

@@ -1104,7 +1113,7 @@ module Json =
11041113

11051114
/// Serializes a value to JSON using a previously compiled codec.
11061115
let serialize (codec: Codec<'T>) (value: 'T) =
1107-
let writer = ResizableBuffer.Create(128)
1116+
let writer = ResizableBuffer.Create(defaultSerializeBufferCapacity)
11081117
codec.Encode writer value
11091118
Encoding.UTF8.GetString(writer.InternalData, 0, writer.InternalCount)
11101119

0 commit comments

Comments
 (0)