From 5c773f5e8c9ab34a82d0707c552ed4ea3d743a09 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 24 Jun 2026 18:56:44 +0300 Subject: [PATCH] v3: speed up cgen x2.5 (-200 MB RAM) --- vlib/v3/bench/bench.v | 13 +++-- vlib/v3/gen/c/cleanc.v | 6 ++- vlib/v3/gen/c/fn_d_parallel.v | 1 + vlib/v3/gen/c/struct.v | 48 ++++++++++++++++-- vlib/v3/transform/expr.v | 10 ++++ vlib/v3/transform/transform.v | 92 +++++++++++++++++++++++++++-------- vlib/v3/v3.v | 3 +- 7 files changed, 142 insertions(+), 31 deletions(-) diff --git a/vlib/v3/bench/bench.v b/vlib/v3/bench/bench.v index 3feac6c93..eb209bfff 100644 --- a/vlib/v3/bench/bench.v +++ b/vlib/v3/bench/bench.v @@ -37,14 +37,21 @@ pub fn new() Bench { } } -// step supports step handling for Bench. +// step records a serial pipeline step. pub fn (mut b Bench) step(name string) { + b.step_parallel(name, false) +} + +// step_parallel records a pipeline step, appending "(parallel)" to its name +// when the step actually ran across threads. +pub fn (mut b Bench) step_parallel(name string, parallel bool) { elapsed_us := b.step_sw.elapsed().microseconds() ram_mb := f64(current_rss_kb()) / 1024.0 ms := f64(elapsed_us) / 1000.0 - println(' ${name:-20s} ${ms:8.2f} ms ${ram_mb:6.0f} MB resident RAM') + label := if parallel { '${name} (parallel)' } else { name } + println(' ${label:-20s} ${ms:8.2f} ms ${ram_mb:6.0f} MB resident RAM') b.steps << Step{ - name: name + name: label time_us: elapsed_us ram_kb: i64(ram_mb * 1024) } diff --git a/vlib/v3/gen/c/cleanc.v b/vlib/v3/gen/c/cleanc.v index 2227e9786..9158ee9b1 100644 --- a/vlib/v3/gen/c/cleanc.v +++ b/vlib/v3/gen/c/cleanc.v @@ -61,7 +61,8 @@ mut: emitted_optional_types map[string]bool emitted_fns map[string]bool array_method_cache map[string]string - param_types_cache map[string][]types.Type // (name|fallback) -> resolved param types + param_types_cache map[string][]types.Type // (name|fallback) -> resolved param types + embedded_fields_by_type map[string][]types.StructField // type name -> its embedded fields (usually empty) spawn_wrapper_names map[string]string spawn_wrapper_defs []string parallel_used bool @@ -117,6 +118,7 @@ pub fn FlatGen.new() FlatGen { emitted_fns: map[string]bool{} array_method_cache: map[string]string{} param_types_cache: map[string][]types.Type{} + embedded_fields_by_type: map[string][]types.StructField{} spawn_wrapper_names: map[string]string{} spawn_wrapper_defs: []string{} str_lits: []string{} @@ -186,6 +188,7 @@ pub fn (mut g FlatGen) gen_with_used_options(a &flat.FlatAst, used_fns map[strin g.emitted_fns = map[string]bool{} g.array_method_cache = map[string]string{} g.param_types_cache = map[string][]types.Type{} + g.embedded_fields_by_type = map[string][]types.StructField{} g.spawn_wrapper_names = map[string]string{} g.spawn_wrapper_defs = []string{} g.parallel_used = false @@ -195,6 +198,7 @@ pub fn (mut g FlatGen) gen_with_used_options(a &flat.FlatAst, used_fns map[strin } g.has_builtins = g.tc.has_builtins g.collect_gen_info() + g.precompute_embedded_fields() g.collect_interface_impls() g.preseed_struct_fn_ptr_types() g.preseed_global_fn_ptr_types() diff --git a/vlib/v3/gen/c/fn_d_parallel.v b/vlib/v3/gen/c/fn_d_parallel.v index 1bcbe2cc7..0c1872fe5 100644 --- a/vlib/v3/gen/c/fn_d_parallel.v +++ b/vlib/v3/gen/c/fn_d_parallel.v @@ -372,6 +372,7 @@ fn (g &FlatGen) new_parallel_worker(worker_id int) &FlatGen { emitted_fns: g.emitted_fns.clone() array_method_cache: g.array_method_cache.clone() param_types_cache: g.param_types_cache.clone() + embedded_fields_by_type: g.embedded_fields_by_type.clone() } } diff --git a/vlib/v3/gen/c/struct.v b/vlib/v3/gen/c/struct.v index 733c38d99..c166ce19c 100644 --- a/vlib/v3/gen/c/struct.v +++ b/vlib/v3/gen/c/struct.v @@ -869,6 +869,47 @@ fn (g &FlatGen) struct_field_type(type_name string, field_name string) ?types.Ty return none } +// precompute_embedded_fields records, per struct type, only its embedded fields (those +// whose field name is the embedded type name). Most structs have none. Done once so the +// per-selector embedded-field resolution doesn't rescan (and re-c_name) every field of +// the receiver struct on every field access — a major cgen cost after #27538. +fn (mut g FlatGen) precompute_embedded_fields() { + for type_name, fields in g.tc.structs { + mut emb := []types.StructField{} + for field in fields { + if g.embedded_field_type_name(field).len > 0 { + emb << field + } + } + g.embedded_fields_by_type[type_name] = emb + } +} + +// struct_embedded_fields returns the embedded fields of a type (mirrors +// struct_fields_for_type's key resolution against the precomputed map). Returns an empty +// slice for non-embedding structs, which is the common case. +fn (g &FlatGen) struct_embedded_fields(type_name string) []types.StructField { + if emb := g.embedded_fields_by_type[type_name] { + return emb + } + qname := g.tc.qualify_name(type_name) + if emb := g.embedded_fields_by_type[qname] { + return emb + } + if info := g.find_struct_decl(type_name) { + if emb := g.embedded_fields_by_type[info.full_name] { + return emb + } + } + if type_name.contains('.') { + short_name := type_name.all_after_last('.') + if emb := g.embedded_fields_by_type[short_name] { + return emb + } + } + return [] +} + fn (g &FlatGen) struct_fields_for_type(type_name string) ?[]types.StructField { if fields := g.tc.structs[type_name] { return fields @@ -935,8 +976,8 @@ fn (g &FlatGen) direct_embedded_field_for_selector(base_type types.Type, field_n if type_name.len == 0 { return none } - fields := g.struct_fields_for_type(type_name) or { return none } - for field in fields { + // Only the embedded fields (precomputed) can match — no need to scan every field. + for field in g.struct_embedded_fields(type_name) { embedded_type_name := g.embedded_field_type_name(field) if embedded_type_name.len == 0 { continue @@ -955,8 +996,7 @@ fn (g &FlatGen) direct_embedded_field_for_selector(base_type types.Type, field_n } fn (g &FlatGen) embedded_field_path_for_promoted_field(type_name string, field_name string) ?[]types.StructField { - fields := g.struct_fields_for_type(type_name) or { return none } - for field in fields { + for field in g.struct_embedded_fields(type_name) { embedded_type_name := g.embedded_field_type_name(field) if embedded_type_name.len == 0 { continue diff --git a/vlib/v3/transform/expr.v b/vlib/v3/transform/expr.v index 2514a6d52..72da67853 100644 --- a/vlib/v3/transform/expr.v +++ b/vlib/v3/transform/expr.v @@ -1,6 +1,7 @@ module transform import v3.flat +import v3.types // transform_infix_string_ops transforms transform infix string ops data for transform. 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 { if type_name.len == 0 { return '' } + // Primitives, arrays and maps are never struct names. Bail before the qualified-name + // concatenation below — this runs for every infix operand, so the saved allocation + // matters. (Behaviour is unchanged: these always resolved to '' anyway.) + first := type_name[0] + if first == `[` + || (first >= `a` && first <= `z` && types.is_builtin_type_name(type_name)) + || type_name.starts_with('map[') { + return '' + } if type_name.contains('.') { if type_name in t.structs { return type_name diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 8dfc99d0a..3bc1d03f4 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -143,6 +143,18 @@ pub fn transform_with_used(mut a flat.FlatAst, tc &types.TypeChecker, used_fns m mut augmented_used_fns := used_fns.clone() mut t := new_transformer(mut a, tc, augmented_used_fns) t.prepare() + // Transform roughly grows the node/children arrays by ~75%. Reserve that capacity + // up front so they don't double past it (the parsed AST already overshoots to the + // next power of two, wasting ~40MB, and each doubling briefly holds both the old and + // new arrays — the dominant peak-RSS contributor under -gc none). + reserve_nodes := a.nodes.len * 7 / 4 - a.nodes.cap + if reserve_nodes > 0 { + unsafe { a.nodes.grow_cap(reserve_nodes) } + } + reserve_children := a.children.len * 7 / 4 - a.children.cap + if reserve_children > 0 { + unsafe { a.children.grow_cap(reserve_children) } + } t.transform_all() return augmented_used_fns } @@ -3121,6 +3133,7 @@ fn (mut t Transformer) transform_children_expr(id flat.NodeId, node flat.Node) f return id } mut new_children := []flat.NodeId{cap: int(node.children_count)} + mut changed := false for i in 0 .. node.children_count { child_id := t.a.child(&node, i) if int(child_id) < 0 { @@ -3132,13 +3145,24 @@ fn (mut t Transformer) transform_children_expr(id flat.NodeId, node flat.Node) f expanded := t.transform_stmt(child_id) if expanded.len == 1 { new_children << expanded[0] + if expanded[0] != child_id { + changed = true + } } else { new_children << t.make_block(expanded) + changed = true } } else { - new_children << t.transform_expr(child_id) + nc := t.transform_expr(child_id) + new_children << nc + if nc != child_id { + changed = true + } } } + if !changed { + return id + } start := t.a.children.len for nc in new_children { t.a.children << nc @@ -3207,6 +3231,12 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat if struct_result := t.transform_transformed_struct_eq(node, new_lhs, new_rhs) { return struct_result } + if new_lhs == lhs_id && new_rhs == rhs_id { + // Nothing was lowered (the common case for plain arithmetic): reuse the original + // node instead of allocating an identical copy. Under -gc none these copies are + // never freed, so avoiding them cuts both transform time and peak RAM. + return id + } start := t.a.children.len t.a.children << new_lhs t.a.children << new_rhs @@ -3314,6 +3344,7 @@ fn (mut t Transformer) transform_index_expr(id flat.NodeId, node flat.Node) flat return lowered } mut new_children := []flat.NodeId{cap: int(node.children_count)} + mut changed := false for i in 0 .. node.children_count { child_id := t.a.child(&node, i) 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 } } } + if new_child != child_id { + changed = true + } new_children << new_child } + // Children unchanged: update the type annotation in place (applying the same + // `typ = node.value when empty` fixup the rebuild would) instead of copying the node. + if !changed { + if node.typ.len == 0 && node.value.len > 0 { + t.a.nodes[int(id)].typ = node.value + } + return id + } start := t.a.children.len for nc in new_children { t.a.children << nc @@ -3541,19 +3583,31 @@ fn (mut t Transformer) transform_selector_expr(id flat.NodeId, node flat.Node) f return t.lower_sum_shared_field_selector(new_base, base_type0, node.value, shared_typ) } new_base := t.transform_expr(base_id) + mut changed := new_base != base_id mut new_children := []flat.NodeId{cap: int(node.children_count)} new_children << new_base for i in 1 .. node.children_count { child_id := t.a.child(&node, i) - new_children << t.transform_expr(child_id) + nc := t.transform_expr(child_id) + if nc != child_id { + changed = true + } + new_children << nc + } + sel_typ := if node.typ.len > 0 { node.typ } else { t.resolve_selector_type(node) } + base_type := t.node_type(base_id) + sel_op := if node.op == .arrow || base_type.starts_with('&') { flat.Op.arrow } else { node.op } + if !changed && sel_op == node.op { + // Children and op unchanged; only the type annotation may differ. Update it in + // place rather than allocating an identical copy (cuts -gc none peak RAM). (`op` + // is an immutable Node field, so a differing op still needs a fresh node below.) + t.a.nodes[int(id)].typ = sel_typ + return id } start := t.a.children.len for nc in new_children { t.a.children << nc } - sel_typ := if node.typ.len > 0 { node.typ } else { t.resolve_selector_type(node) } - base_type := t.node_type(base_id) - sel_op := if node.op == .arrow || base_type.starts_with('&') { flat.Op.arrow } else { node.op } return t.a.add_node(flat.Node{ kind: .selector op: sel_op @@ -4298,24 +4352,18 @@ fn (mut t Transformer) transform_ident_expr(id flat.NodeId, node flat.Node) flat if smartcasted := t.smartcast_ident_value(node.value) { return smartcasted } + // Idents are the most common node; re-annotating them in place (rather than + // allocating a fresh node) avoids cascading rebuilds of every enclosing + // expression and the associated allocations (critical under -gc none). if !t.in_call_callee { if fn_name := t.resolve_fn_value_ident(node.value) { - return t.a.add_node(flat.Node{ - kind: .ident - value: fn_name - typ: node.typ - pos: node.pos - }) + t.a.nodes[int(id)].value = fn_name + return id } } typ := t.var_type(node.value) - if typ.len > 0 { - return t.a.add_node(flat.Node{ - kind: .ident - value: node.value - typ: typ - pos: node.pos - }) + if typ.len > 0 && typ != node.typ { + t.a.nodes[int(id)].typ = typ } return id } @@ -5418,8 +5466,11 @@ fn (mut t Transformer) build_match_chain(match_expr_id flat.NodeId, orig_expr_id branch := t.a.nodes[int(branches[idx])] is_else := branch.value == 'else' - body_start_idx := if is_else { 0 } else { t.count_conds(branch) } - if !is_else && t.match_branch_all_type_patterns(branch) && t.count_conds(branch) > 1 { + // count_conds scans the branch's condition children; compute it once and reuse + // (build_match_chain runs per branch, and the compiler has very large matches). + n_conds := if is_else { 0 } else { t.count_conds(branch) } + body_start_idx := n_conds + if !is_else && n_conds > 1 && t.match_branch_all_type_patterns(branch) { return t.build_match_type_branch_chain(match_expr_id, orig_expr_id, branch, branches, idx, 0) } @@ -5427,7 +5478,6 @@ fn (mut t Transformer) build_match_chain(match_expr_id flat.NodeId, orig_expr_id // single sum-type variant, so selectors inside the body get narrowed. mut sc_pushed := 0 if !is_else { - n_conds := t.count_conds(branch) if n_conds == 1 { cond_val_id := t.a.child(&branch, 0) if variant_name := t.match_type_pattern(cond_val_id) { diff --git a/vlib/v3/v3.v b/vlib/v3/v3.v index 992c5f687..9fd7a9f5c 100644 --- a/vlib/v3/v3.v +++ b/vlib/v3/v3.v @@ -324,8 +324,7 @@ fn main() { eprintln('error writing ${output_file}') exit(1) } - gen_step_name := if g.was_parallel() { 'gen C/write (parallel)' } else { 'gen C/write' } - b.step(gen_step_name) + b.step_parallel('gen C/write', g.was_parallel()) if c_only { b.print_report() return -- 2.39.5