Summary
sql db { select from M } expands into a per-model set of generic instantiations (new_query[T], new_query_internal[T], struct_meta[T], table_from_struct[T] and QueryBuilder[T] methods including map_row). The cost is essentially fixed per distinct model type, not per query:
- 1 model, 1 select → 0.65 s in
monomorphize, 719 KB C, 14orm__symbols for that one model - 47 models, 1 select each → 8.92 s, 6.39 MB C
- 47 models, 3 selects each → 11.22 s (+2.3 s for 2× the query sites)
Slope: ~0.19 s and ~120 KB of C per added model type.
Environment
- V 0.5.2
5bd6709(vlang/v master), Linux x86_64 v -new-compiler -cc tcc -no-retry-compilation- Compile to C (
-o out.c) so only the V pipeline stages are measured; timings from-show-timings; ±5% run-to-run variance - No database needed — the repro only compiles
Minimal case (single file, one model)
module main
import orm
import time
import os
@[table: 't0']
pub struct M0 {
pub:
id string @[primary; immutable; sql_type: 'CHAR(36)']
name string @[sql_type: 'VARCHAR(255)']
code string @[sql_type: 'VARCHAR(64)']
qty int
sort u32
status u8
score f64
rate f64
flag bool
since ?int
note ?string
creator_id ?string
created_at time.Time
updated_at time.Time
deleted_at ?time.Time
}
fn q0(db orm.Connection) ![]M0 {
return sql db { select from M0 }!
}
fn bench_all() {
db := unsafe { &orm.Connection(nil) }
_ = q0(db) or { []M0{} }
}
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 645.40 ms
cgen (parallel) 115.65 ms
total 1359.83 ms
generated C size 718542 bytesThe 14 symbols instantiated for this single select-only model:
orm__QueryBuilder_M0
orm__QueryBuilder_M0__ensure_keys_for_includes
orm__QueryBuilder_M0__map_row
orm__QueryBuilder_M0__prepare
orm__QueryBuilder_M0__query
orm__QueryBuilder_M0__reset
orm__QueryBuilder_M0__selected_primary_value
orm__QueryBuilder_M0__v_sql_field_name
orm__QueryBuilder_M0__v_sql_select_fields
orm__QueryBuilder_M0__v_sql_table_attrs
orm__new_query_T_M0
orm__new_query_internal_T_M0
orm__struct_meta_T_M0
orm__table_from_struct_T_M0Scaling reproduction
gen.sh generates N models, each used by exactly one select:
#!/bin/sh
# Usage: sh gen.sh 47 > main.v
N=${1:-47}
cat <<'HDR'
module main
import orm
import time
import os
HDR
i=0
while [ "$i" -lt "$N" ]; do
cat <<EOF
@[table: 't$i']
pub struct M$i {
pub:
id string @[primary; immutable; sql_type: 'CHAR(36)']
name string @[sql_type: 'VARCHAR(255)']
code string @[sql_type: 'VARCHAR(64)']
qty int
sort u32
status u8
score f64
rate f64
flag bool
since ?int
note ?string
creator_id ?string
created_at time.Time
updated_at time.Time
deleted_at ?time.Time
}
fn q$i(db orm.Connection) ![]M$i {
return sql db { select from M$i }!
}
EOF
i=$((i + 1))
done
echo ''
echo 'fn bench_all() {'
echo ' db := unsafe { &orm.Connection(nil) }'
i=0
while [ "$i" -lt "$N" ]; do
echo " _ = q$i(db) or { []M$i{} }"
i=$((i + 1))
done
echo '}'
echo ''
echo 'fn main() {'
echo ' if os.args.len > 100 {'
echo ' bench_all()'
echo ' }'
echo '}'$ sh gen.sh 47 > main.v
$ v -new-compiler -cc tcc -no-retry-compilation -show-timings -o out.c main.v
monomorphize 8918.86 ms 271 MB RSS 831 MB stage peak
cgen (parallel) 850.77 ms 248 MB RSS 327 MB stage peak
total 10447.28 ms
generated C size 6389867 bytes| models | query sites | monomorphize | cgen | total | generated C |
|---|---|---|---|---|---|
| 1 | 1 × select | 0.65 s | 0.12 s | 1.36 s | 0.72 MB |
| 8 | 8 × select | 1.53 s | 0.22 s | 2.36 s | 1.58 MB |
| 47 | 47 × select | 8.92 s | 0.85 s | 10.45 s | 6.39 MB |
| 47 | 47 × 3 selects | 11.22 s | 1.13 s | 13.16 s | 7.62 MB |
The 3-select variant only adds two lines to each q$i:
_ := sql db { select count from M$i }!
_ := sql db { select from M$i where id == 'x' limit 1 }!Expected
Per-model cost should be much lower or amortized across models — e.g. row mapping and metadata for a given struct shape generated once and shared, instead of ~14 generic instantiations per model with map_row being the largest.
Where the cost is
vlib/orm/orm_func.v:922struct_meta[T]()— comptime$for field in T.fieldsreflectionvlib/orm/orm_func.v:1047map_row[T]— in a real 47-model app this single family is 111k C lines (~2.4k lines per model)vlib/orm/orm_func.v:81/85new_query[T]/new_query_internal[T]- The time is in
monomorphize(8.9 s) rather thancgen(0.85 s), so the bottleneck is specialization in the transform stage, not C emission —drain_pending_generic_fn_specsprofiling per spec may be the right place to look
Impact
A private 24k-line veb + orm + json2 app: ORM functions are ~197k of 528k generated C lines (37%); an identical second build still spends 73 s in monomorphize + 40 s in cgen.
Notes
- Steady-state cost, not the non-convergence/hang fixed in #29764.
- Related: #29304 / #29311 (comptime reflection AST copies, fixed), #29354 / #29364 (json2 per-struct cost, fixed).
Happy to test patches or provide the generated main.v for any N.
Activity
0No activity yet.