Skip to content

Commit 5c773f5

Browse files
committed
v3: speed up cgen x2.5 (-200 MB RAM)
1 parent beca086 commit 5c773f5

7 files changed

Lines changed: 142 additions & 31 deletions

File tree

vlib/v3/bench/bench.v

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,21 @@ pub fn new() Bench {
3737
}
3838
}
3939

40-
// step supports step handling for Bench.
40+
// step records a serial pipeline step.
4141
pub fn (mut b Bench) step(name string) {
42+
b.step_parallel(name, false)
43+
}
44+
45+
// step_parallel records a pipeline step, appending "(parallel)" to its name
46+
// when the step actually ran across threads.
47+
pub fn (mut b Bench) step_parallel(name string, parallel bool) {
4248
elapsed_us := b.step_sw.elapsed().microseconds()
4349
ram_mb := f64(current_rss_kb()) / 1024.0
4450
ms := f64(elapsed_us) / 1000.0
45-
println(' ${name:-20s} ${ms:8.2f} ms ${ram_mb:6.0f} MB resident RAM')
51+
label := if parallel { '${name} (parallel)' } else { name }
52+
println(' ${label:-20s} ${ms:8.2f} ms ${ram_mb:6.0f} MB resident RAM')
4653
b.steps << Step{
47-
name: name
54+
name: label
4855
time_us: elapsed_us
4956
ram_kb: i64(ram_mb * 1024)
5057
}

vlib/v3/gen/c/cleanc.v

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ mut:
6161
emitted_optional_types map[string]bool
6262
emitted_fns map[string]bool
6363
array_method_cache map[string]string
64-
param_types_cache map[string][]types.Type // (name|fallback) -> resolved param types
64+
param_types_cache map[string][]types.Type // (name|fallback) -> resolved param types
65+
embedded_fields_by_type map[string][]types.StructField // type name -> its embedded fields (usually empty)
6566
spawn_wrapper_names map[string]string
6667
spawn_wrapper_defs []string
6768
parallel_used bool
@@ -117,6 +118,7 @@ pub fn FlatGen.new() FlatGen {
117118
emitted_fns: map[string]bool{}
118119
array_method_cache: map[string]string{}
119120
param_types_cache: map[string][]types.Type{}
121+
embedded_fields_by_type: map[string][]types.StructField{}
120122
spawn_wrapper_names: map[string]string{}
121123
spawn_wrapper_defs: []string{}
122124
str_lits: []string{}
@@ -186,6 +188,7 @@ pub fn (mut g FlatGen) gen_with_used_options(a &flat.FlatAst, used_fns map[strin
186188
g.emitted_fns = map[string]bool{}
187189
g.array_method_cache = map[string]string{}
188190
g.param_types_cache = map[string][]types.Type{}
191+
g.embedded_fields_by_type = map[string][]types.StructField{}
189192
g.spawn_wrapper_names = map[string]string{}
190193
g.spawn_wrapper_defs = []string{}
191194
g.parallel_used = false
@@ -195,6 +198,7 @@ pub fn (mut g FlatGen) gen_with_used_options(a &flat.FlatAst, used_fns map[strin
195198
}
196199
g.has_builtins = g.tc.has_builtins
197200
g.collect_gen_info()
201+
g.precompute_embedded_fields()
198202
g.collect_interface_impls()
199203
g.preseed_struct_fn_ptr_types()
200204
g.preseed_global_fn_ptr_types()

vlib/v3/gen/c/fn_d_parallel.v

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ fn (g &FlatGen) new_parallel_worker(worker_id int) &FlatGen {
372372
emitted_fns: g.emitted_fns.clone()
373373
array_method_cache: g.array_method_cache.clone()
374374
param_types_cache: g.param_types_cache.clone()
375+
embedded_fields_by_type: g.embedded_fields_by_type.clone()
375376
}
376377
}
377378

vlib/v3/gen/c/struct.v

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,47 @@ fn (g &FlatGen) struct_field_type(type_name string, field_name string) ?types.Ty
869869
return none
870870
}
871871

872+
// precompute_embedded_fields records, per struct type, only its embedded fields (those
873+
// whose field name is the embedded type name). Most structs have none. Done once so the
874+
// per-selector embedded-field resolution doesn't rescan (and re-c_name) every field of
875+
// the receiver struct on every field access — a major cgen cost after #27538.
876+
fn (mut g FlatGen) precompute_embedded_fields() {
877+
for type_name, fields in g.tc.structs {
878+
mut emb := []types.StructField{}
879+
for field in fields {
880+
if g.embedded_field_type_name(field).len > 0 {
881+
emb << field
882+
}
883+
}
884+
g.embedded_fields_by_type[type_name] = emb
885+
}
886+
}
887+
888+
// struct_embedded_fields returns the embedded fields of a type (mirrors
889+
// struct_fields_for_type's key resolution against the precomputed map). Returns an empty
890+
// slice for non-embedding structs, which is the common case.
891+
fn (g &FlatGen) struct_embedded_fields(type_name string) []types.StructField {
892+
if emb := g.embedded_fields_by_type[type_name] {
893+
return emb
894+
}
895+
qname := g.tc.qualify_name(type_name)
896+
if emb := g.embedded_fields_by_type[qname] {
897+
return emb
898+
}
899+
if info := g.find_struct_decl(type_name) {
900+
if emb := g.embedded_fields_by_type[info.full_name] {
901+
return emb
902+
}
903+
}
904+
if type_name.contains('.') {
905+
short_name := type_name.all_after_last('.')
906+
if emb := g.embedded_fields_by_type[short_name] {
907+
return emb
908+
}
909+
}
910+
return []
911+
}
912+
872913
fn (g &FlatGen) struct_fields_for_type(type_name string) ?[]types.StructField {
873914
if fields := g.tc.structs[type_name] {
874915
return fields
@@ -935,8 +976,8 @@ fn (g &FlatGen) direct_embedded_field_for_selector(base_type types.Type, field_n
935976
if type_name.len == 0 {
936977
return none
937978
}
938-
fields := g.struct_fields_for_type(type_name) or { return none }
939-
for field in fields {
979+
// Only the embedded fields (precomputed) can match — no need to scan every field.
980+
for field in g.struct_embedded_fields(type_name) {
940981
embedded_type_name := g.embedded_field_type_name(field)
941982
if embedded_type_name.len == 0 {
942983
continue
@@ -955,8 +996,7 @@ fn (g &FlatGen) direct_embedded_field_for_selector(base_type types.Type, field_n
955996
}
956997

957998
fn (g &FlatGen) embedded_field_path_for_promoted_field(type_name string, field_name string) ?[]types.StructField {
958-
fields := g.struct_fields_for_type(type_name) or { return none }
959-
for field in fields {
999+
for field in g.struct_embedded_fields(type_name) {
9601000
embedded_type_name := g.embedded_field_type_name(field)
9611001
if embedded_type_name.len == 0 {
9621002
continue

vlib/v3/transform/expr.v

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module transform
22

33
import v3.flat
4+
import v3.types
45

56
// transform_infix_string_ops transforms transform infix string ops data for transform.
67
fn (mut t Transformer) transform_infix_string_ops(_id flat.NodeId, node flat.Node) ?flat.NodeId {
@@ -543,6 +544,15 @@ fn (t &Transformer) struct_lookup_name(type_name string) string {
543544
if type_name.len == 0 {
544545
return ''
545546
}
547+
// Primitives, arrays and maps are never struct names. Bail before the qualified-name
548+
// concatenation below — this runs for every infix operand, so the saved allocation
549+
// matters. (Behaviour is unchanged: these always resolved to '' anyway.)
550+
first := type_name[0]
551+
if first == `[`
552+
|| (first >= `a` && first <= `z` && types.is_builtin_type_name(type_name))
553+
|| type_name.starts_with('map[') {
554+
return ''
555+
}
546556
if type_name.contains('.') {
547557
if type_name in t.structs {
548558
return type_name

vlib/v3/transform/transform.v

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,18 @@ pub fn transform_with_used(mut a flat.FlatAst, tc &types.TypeChecker, used_fns m
143143
mut augmented_used_fns := used_fns.clone()
144144
mut t := new_transformer(mut a, tc, augmented_used_fns)
145145
t.prepare()
146+
// Transform roughly grows the node/children arrays by ~75%. Reserve that capacity
147+
// up front so they don't double past it (the parsed AST already overshoots to the
148+
// next power of two, wasting ~40MB, and each doubling briefly holds both the old and
149+
// new arrays — the dominant peak-RSS contributor under -gc none).
150+
reserve_nodes := a.nodes.len * 7 / 4 - a.nodes.cap
151+
if reserve_nodes > 0 {
152+
unsafe { a.nodes.grow_cap(reserve_nodes) }
153+
}
154+
reserve_children := a.children.len * 7 / 4 - a.children.cap
155+
if reserve_children > 0 {
156+
unsafe { a.children.grow_cap(reserve_children) }
157+
}
146158
t.transform_all()
147159
return augmented_used_fns
148160
}
@@ -3121,6 +3133,7 @@ fn (mut t Transformer) transform_children_expr(id flat.NodeId, node flat.Node) f
31213133
return id
31223134
}
31233135
mut new_children := []flat.NodeId{cap: int(node.children_count)}
3136+
mut changed := false
31243137
for i in 0 .. node.children_count {
31253138
child_id := t.a.child(&node, i)
31263139
if int(child_id) < 0 {
@@ -3132,13 +3145,24 @@ fn (mut t Transformer) transform_children_expr(id flat.NodeId, node flat.Node) f
31323145
expanded := t.transform_stmt(child_id)
31333146
if expanded.len == 1 {
31343147
new_children << expanded[0]
3148+
if expanded[0] != child_id {
3149+
changed = true
3150+
}
31353151
} else {
31363152
new_children << t.make_block(expanded)
3153+
changed = true
31373154
}
31383155
} else {
3139-
new_children << t.transform_expr(child_id)
3156+
nc := t.transform_expr(child_id)
3157+
new_children << nc
3158+
if nc != child_id {
3159+
changed = true
3160+
}
31403161
}
31413162
}
3163+
if !changed {
3164+
return id
3165+
}
31423166
start := t.a.children.len
31433167
for nc in new_children {
31443168
t.a.children << nc
@@ -3207,6 +3231,12 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat
32073231
if struct_result := t.transform_transformed_struct_eq(node, new_lhs, new_rhs) {
32083232
return struct_result
32093233
}
3234+
if new_lhs == lhs_id && new_rhs == rhs_id {
3235+
// Nothing was lowered (the common case for plain arithmetic): reuse the original
3236+
// node instead of allocating an identical copy. Under -gc none these copies are
3237+
// never freed, so avoiding them cuts both transform time and peak RAM.
3238+
return id
3239+
}
32103240
start := t.a.children.len
32113241
t.a.children << new_lhs
32123242
t.a.children << new_rhs
@@ -3314,6 +3344,7 @@ fn (mut t Transformer) transform_index_expr(id flat.NodeId, node flat.Node) flat
33143344
return lowered
33153345
}
33163346
mut new_children := []flat.NodeId{cap: int(node.children_count)}
3347+
mut changed := false
33173348
for i in 0 .. node.children_count {
33183349
child_id := t.a.child(&node, i)
33193350
mut new_child := t.transform_expr(child_id)
@@ -3327,8 +3358,19 @@ fn (mut t Transformer) transform_index_expr(id flat.NodeId, node flat.Node) flat
33273358
}
33283359
}
33293360
}
3361+
if new_child != child_id {
3362+
changed = true
3363+
}
33303364
new_children << new_child
33313365
}
3366+
// Children unchanged: update the type annotation in place (applying the same
3367+
// `typ = node.value when empty` fixup the rebuild would) instead of copying the node.
3368+
if !changed {
3369+
if node.typ.len == 0 && node.value.len > 0 {
3370+
t.a.nodes[int(id)].typ = node.value
3371+
}
3372+
return id
3373+
}
33323374
start := t.a.children.len
33333375
for nc in new_children {
33343376
t.a.children << nc
@@ -3541,19 +3583,31 @@ fn (mut t Transformer) transform_selector_expr(id flat.NodeId, node flat.Node) f
35413583
return t.lower_sum_shared_field_selector(new_base, base_type0, node.value, shared_typ)
35423584
}
35433585
new_base := t.transform_expr(base_id)
3586+
mut changed := new_base != base_id
35443587
mut new_children := []flat.NodeId{cap: int(node.children_count)}
35453588
new_children << new_base
35463589
for i in 1 .. node.children_count {
35473590
child_id := t.a.child(&node, i)
3548-
new_children << t.transform_expr(child_id)
3591+
nc := t.transform_expr(child_id)
3592+
if nc != child_id {
3593+
changed = true
3594+
}
3595+
new_children << nc
3596+
}
3597+
sel_typ := if node.typ.len > 0 { node.typ } else { t.resolve_selector_type(node) }
3598+
base_type := t.node_type(base_id)
3599+
sel_op := if node.op == .arrow || base_type.starts_with('&') { flat.Op.arrow } else { node.op }
3600+
if !changed && sel_op == node.op {
3601+
// Children and op unchanged; only the type annotation may differ. Update it in
3602+
// place rather than allocating an identical copy (cuts -gc none peak RAM). (`op`
3603+
// is an immutable Node field, so a differing op still needs a fresh node below.)
3604+
t.a.nodes[int(id)].typ = sel_typ
3605+
return id
35493606
}
35503607
start := t.a.children.len
35513608
for nc in new_children {
35523609
t.a.children << nc
35533610
}
3554-
sel_typ := if node.typ.len > 0 { node.typ } else { t.resolve_selector_type(node) }
3555-
base_type := t.node_type(base_id)
3556-
sel_op := if node.op == .arrow || base_type.starts_with('&') { flat.Op.arrow } else { node.op }
35573611
return t.a.add_node(flat.Node{
35583612
kind: .selector
35593613
op: sel_op
@@ -4298,24 +4352,18 @@ fn (mut t Transformer) transform_ident_expr(id flat.NodeId, node flat.Node) flat
42984352
if smartcasted := t.smartcast_ident_value(node.value) {
42994353
return smartcasted
43004354
}
4355+
// Idents are the most common node; re-annotating them in place (rather than
4356+
// allocating a fresh node) avoids cascading rebuilds of every enclosing
4357+
// expression and the associated allocations (critical under -gc none).
43014358
if !t.in_call_callee {
43024359
if fn_name := t.resolve_fn_value_ident(node.value) {
4303-
return t.a.add_node(flat.Node{
4304-
kind: .ident
4305-
value: fn_name
4306-
typ: node.typ
4307-
pos: node.pos
4308-
})
4360+
t.a.nodes[int(id)].value = fn_name
4361+
return id
43094362
}
43104363
}
43114364
typ := t.var_type(node.value)
4312-
if typ.len > 0 {
4313-
return t.a.add_node(flat.Node{
4314-
kind: .ident
4315-
value: node.value
4316-
typ: typ
4317-
pos: node.pos
4318-
})
4365+
if typ.len > 0 && typ != node.typ {
4366+
t.a.nodes[int(id)].typ = typ
43194367
}
43204368
return id
43214369
}
@@ -5418,16 +5466,18 @@ fn (mut t Transformer) build_match_chain(match_expr_id flat.NodeId, orig_expr_id
54185466
branch := t.a.nodes[int(branches[idx])]
54195467
is_else := branch.value == 'else'
54205468

5421-
body_start_idx := if is_else { 0 } else { t.count_conds(branch) }
5422-
if !is_else && t.match_branch_all_type_patterns(branch) && t.count_conds(branch) > 1 {
5469+
// count_conds scans the branch's condition children; compute it once and reuse
5470+
// (build_match_chain runs per branch, and the compiler has very large matches).
5471+
n_conds := if is_else { 0 } else { t.count_conds(branch) }
5472+
body_start_idx := n_conds
5473+
if !is_else && n_conds > 1 && t.match_branch_all_type_patterns(branch) {
54235474
return t.build_match_type_branch_chain(match_expr_id, orig_expr_id, branch, branches, idx,
54245475
0)
54255476
}
54265477
// Push a smartcast around the body transform when this branch matches a
54275478
// single sum-type variant, so selectors inside the body get narrowed.
54285479
mut sc_pushed := 0
54295480
if !is_else {
5430-
n_conds := t.count_conds(branch)
54315481
if n_conds == 1 {
54325482
cond_val_id := t.a.child(&branch, 0)
54335483
if variant_name := t.match_type_pattern(cond_val_id) {

vlib/v3/v3.v

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,7 @@ fn main() {
324324
eprintln('error writing ${output_file}')
325325
exit(1)
326326
}
327-
gen_step_name := if g.was_parallel() { 'gen C/write (parallel)' } else { 'gen C/write' }
328-
b.step(gen_step_name)
327+
b.step_parallel('gen C/write', g.was_parallel())
329328
if c_only {
330329
b.print_report()
331330
return

0 commit comments

Comments
 (0)