Summary
This is a follow-up to #29354 (closed by #29364). The fix already cut the per-struct cost roughly 6× — thanks for that. This issue tracks what remains on current master:
- 1 struct with
decode+encode→ 0.89 s inmonomorphize, 912 KB of generated C, 9json2__symbols for that one struct - 200 structs → 12.28 s in
monomorphize, 5.01 MB of C - marginal cost: ~57 ms of monomorphization and ~21 KB of C per additional struct, essentially linear
The cost is per distinct struct type; source size barely changes between N=1 and N=200.
Environment
- V 0.5.2
5bd6709(vlang/v master), Linux x86_64 — includes the #29364 fix (b8194d12ec) v -new-compiler -cc tcc -no-retry-compilation- Compiled to C (
-o out.c), so only the V pipeline stages are measured;-show-timings; ±5% run-to-run variance - No runtime needed — the reproducer only compiles
Minimal case (single file, one struct)
module main
import json2
import time
import os
pub struct D0 {
f0_name string
f0_opt ?string
f0_num int
f0_small u8
f0_rate f64
f0_ok bool
f0_tags []string
f0_at time.Time
f0_extra ?int
f0_text string
f0_list []int
f0_score f64
}
fn bench_all() {
s0 := '{}'
d0 := json2.decode[D0](s0) or { D0{} }
_ = json2.encode(d0)
}
fn main() {
if os.args.len > 100 {
bench_all()
}
}$ v -new-compiler -cc tcc -no-retry-compilation -show-timings -o out.c minimal.v
monomorphize 888.00 ms 196 MB RSS 291 MB stage peak
cgen (parallel) 125.85 ms 183 MB RSS 224 MB stage peak
total 1802.08 ms
generated C size 912479 bytesThe 9 symbols instantiated for this single struct:
json2__Decoder_D0__cached_struct_field_infos
json2__Decoder_D0__decode_root
json2__Decoder_D0__decode_struct_fields
json2__Decoder_D0__decode_value
json2__Encoder_D0__cached_field_infos
json2__Encoder_D0__encode_struct_fields
json2__Encoder_D0__encode_value
json2__decode_T_D0
json2__encode_T_D0Scaling reproduction
#!/bin/sh
# Usage: sh gen.sh 200 > main.v
N=${1:-200}
cat <<'HDR'
module main
import json2
import time
import os
HDR
i=0
while [ "$i" -lt "$N" ]; do
cat <<EOF
pub struct D$i {
f${i}_name string
f${i}_opt ?string
f${i}_num int
f${i}_small u8
f${i}_rate f64
f${i}_ok bool
f${i}_tags []string
f${i}_at time.Time
f${i}_extra ?int
f${i}_text string
f${i}_list []int
f${i}_score f64
}
EOF
i=$((i + 1))
done
echo ''
echo 'fn bench_all() {'
i=0
while [ "$i" -lt "$N" ]; do
echo " s$i := '{}'"
echo " d$i := json2.decode[D$i](s$i) or { D$i{} }"
echo " _ = json2.encode(d$i)"
i=$((i + 1))
done
echo '}'
echo ''
echo 'fn main() {'
echo ' if os.args.len > 100 {'
echo ' bench_all()'
echo ' }'
echo '}'$ sh gen.sh 200 > main.v
$ v -new-compiler -cc tcc -no-retry-compilation -show-timings -o out.c main.v
monomorphize 12280.53 ms 325 MB RSS 1236 MB stage peak
cgen (parallel) 2506.47 ms 287 MB RSS 547 MB stage peak
total 15836.55 ms
generated C size 5011631 bytes| structs | monomorphize | generated C |
|---|---|---|
| 1 | 0.89 s | 0.91 MB |
| 8 | 1.29 s | 1.05 MB |
| 32 | 2.67 s | 1.55 MB |
| 96 | 6.31 s | 2.86 MB |
| 200 | 12.28 s | 5.01 MB |
Roughly linear: ~57 ms and ~21 KB of C per additional struct on top of a ~0.8 s framework floor.
Generic wrapper variant
Wrapping every encoded value in a generic ApiSuccessResponse[T] instead of encoding T directly (same 200 structs):
| case | monomorphize | total | generated C |
|---|---|---|---|
encode D_i directly | 12.28 s | 15.84 s | 5.01 MB |
encode ApiSuccessResponse[D_i] | 17.51 s | 25.93 s | 6.60 MB |
So a generic response envelope adds ~24 ms and ~8 KB of C per wrapper type.
Impact
A real 24k-line veb + orm + json2 app has 216 distinct json2.encode and 221 distinct json2.decode instantiations; json2 functions are ~121k of 528k generated C lines (23%). 181 of the encode instantiations are just ApiSuccessResponse[T] wrappers, and an identical second build still spends 73 s in monomorphize + 40 s in cgen.
Where the cost is
vlib/json2/encode.v:61encode_value[T],vlib/json2/encode.v:986encode_struct_fields[T]vlib/json2/decode.v:391decode[T](plus thedecode_root/decode_struct_fieldshelpers per struct)- The
cached_field_infos/cached_struct_field_infoshelpers suggest field metadata is already cached at runtime, but the per-struct generated code (and its specialization cost) remains
Expected
Any of these would help apps with 100+ DTO types:
- share generated field access/encoding code across structs with the same field-type shape, instead of specializing everything per struct
- a cheaper path for wrapper generics like
ApiSuccessResponse[T](e.g. reuse the inner encoder rather than specializing the wrapper's field loop) - or at least keeping the per-struct marginal cost closer to #29364's level for larger N if the current growth is an artifact of the specialization pipeline rather than inherent
Happy to run any patch against both reproducers (and against a 400+ struct variant) and report before/after numbers.
Activity
0No activity yet.