From bd87753646a4c2ae1384399584858dc14cb90b28 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sat, 27 Jun 2026 11:48:15 +0300 Subject: [PATCH] v3: optimize speed and RAM usage --- vlib/v3/gen/c/cleanc.v | 10 +++ vlib/v3/gen/c/fn_d_parallel.v | 7 +- vlib/v3/gen/c/struct.v | 16 +++- vlib/v3/markused/markused.v | 91 ++++++++++++++++++- vlib/v3/transform/transform.v | 20 ++++- vlib/v3/transform/transform_d_parallel.v | 11 ++- vlib/v3/types/checker.v | 106 ++++++++++++++--------- vlib/v3/v3.v | 2 +- 8 files changed, 209 insertions(+), 54 deletions(-) diff --git a/vlib/v3/gen/c/cleanc.v b/vlib/v3/gen/c/cleanc.v index 726d944ed..7a892b4c1 100644 --- a/vlib/v3/gen/c/cleanc.v +++ b/vlib/v3/gen/c/cleanc.v @@ -252,6 +252,8 @@ pub fn (mut g FlatGen) gen_with_used_options(a &flat.FlatAst, used_fns map[strin g.line_start = true g.gen_fns_dispatch(no_parallel) fn_code := g.sb.str() + // `.str()` copies out of the builder; free the emptied backing array under -gc none. + unsafe { g.sb.free() } g.sb = orig_sb g.line_start = orig_line_start g.preamble() @@ -281,6 +283,8 @@ pub fn (mut g FlatGen) gen_with_used_options(a &flat.FlatAst, used_fns map[strin g.interface_method_stubs() g.enum_str_defs() g.sb.write_string(const_code) + // The final builder now owns a copy of the const code. + unsafe { const_code.free() } if g.runtime_inits.len > 0 || g.module_init_fns.len > 0 || g.global_inits.len > 0 { g.writeln('void _vinit() {') for ri in g.runtime_inits { @@ -294,7 +298,11 @@ pub fn (mut g FlatGen) gen_with_used_options(a &flat.FlatAst, used_fns map[strin g.writeln('') } g.sb.write_string(fn_code) + // The final builder now owns a copy of the function code. + unsafe { fn_code.free() } result := g.sb.str() + // Keep only the returned C string, not the builder's copied backing array. + unsafe { g.sb.free() } return result } @@ -3812,6 +3820,8 @@ fn (mut g FlatGen) precompute_consts() string { g.writeln('') } result := g.sb.str() + // `.str()` copies out of the temporary const builder. + unsafe { g.sb.free() } g.sb = old_sb g.line_start = old_line_start return result diff --git a/vlib/v3/gen/c/fn_d_parallel.v b/vlib/v3/gen/c/fn_d_parallel.v index 94ebf6263..3a63f25fc 100644 --- a/vlib/v3/gen/c/fn_d_parallel.v +++ b/vlib/v3/gen/c/fn_d_parallel.v @@ -5,7 +5,7 @@ import strings import v3.flat import v3.types -const max_flat_cgen_jobs = 3 +const max_flat_cgen_jobs = 2 const min_flat_cgen_parallel_items = 1024 // FlatFnGenItem represents flat fn gen item data used by c. @@ -497,6 +497,11 @@ fn (mut g FlatGen) merge_parallel_worker(w &FlatGen) { if worker_output.len > 0 { g.sb.write_string(worker_output) } + // The master builder has copied the worker output; release the worker buffers. + unsafe { + worker_output.free() + ww.sb.free() + } for opt_name, val_type in w.needed_optional_types { g.needed_optional_types[opt_name] = val_type } diff --git a/vlib/v3/gen/c/struct.v b/vlib/v3/gen/c/struct.v index 15647e741..0ffda43f5 100644 --- a/vlib/v3/gen/c/struct.v +++ b/vlib/v3/gen/c/struct.v @@ -613,8 +613,8 @@ fn (mut g FlatGen) gen_struct_default_fields(type_name string, mut set_fields ma g.write(', ') } g.write('.${c_name(field.value)} = ') - g.gen_struct_field_expr_for_field(g.a.child(field, 0), info.full_name, field.value, - g.tc.parse_type(field.typ)) + g.gen_struct_field_expr_for_field(g.a.child(field, 0), info.full_name, field.value, g.struct_default_field_type(info, + field)) set_fields[field.value] = true has = true } @@ -622,6 +622,18 @@ fn (mut g FlatGen) gen_struct_default_fields(type_name string, mut set_fields ma return has } +fn (mut g FlatGen) struct_default_field_type(info StructDeclInfo, field flat.Node) types.Type { + if field.typ.len > 0 && !field.typ.contains('.') && info.module.len > 0 && info.module != 'main' + && info.module != 'builtin' { + qtyp := '${info.module}.${field.typ}' + if qtyp in g.tc.enum_names || qtyp in g.tc.structs || qtyp in g.tc.sum_types + || qtyp in g.tc.interface_names { + return g.tc.parse_type(qtyp) + } + } + return g.tc.parse_type(field.typ) +} + // gen_default_value_for_type emits default value for type output for c. fn (mut g FlatGen) gen_default_value_for_type(typ types.Type) { raw_typ := typ diff --git a/vlib/v3/markused/markused.v b/vlib/v3/markused/markused.v index bf80faf18..295b5c930 100644 --- a/vlib/v3/markused/markused.v +++ b/vlib/v3/markused/markused.v @@ -130,6 +130,7 @@ fn mark_used_with_test_files(a &flat.FlatAst, tc &types.TypeChecker, test_files enqueue_main_module_roots(fn_decls, mut used, mut queue) enqueue_auto_roots(fn_decls, mut used, mut queue) enqueue_export_roots(a, mut used, mut queue) + enqueue_veb_handler_roots(a, tc, mut used, mut queue) enqueue_test_file_roots(a, test_files, mut used, mut queue) queue << 'time.Time.new' used['time.Time.new'] = true @@ -478,6 +479,9 @@ fn enqueue_initializer_calls(a &flat.FlatAst, collector CallCollector, imports m } fn enqueue_top_level_calls(a &flat.FlatAst, collector CallCollector, fn_decls map[string]FnDeclInfo, mut used map[string]bool, mut queue []string) { + if markused_has_entry_main(a) { + return + } mut calls := []string{cap: 32} for file_idx, file_node in a.nodes { if !markused_should_scan_top_level_file(a, file_idx, file_node) { @@ -655,6 +659,77 @@ fn enqueue_export_roots(a &flat.FlatAst, mut used map[string]bool, mut queue []s } } +fn enqueue_veb_handler_roots(a &flat.FlatAst, tc &types.TypeChecker, mut used map[string]bool, mut queue []string) { + mut cur_module := '' + for node in a.nodes { + match node.kind { + .file { + cur_module = '' + } + .module_decl { + cur_module = node.value + } + .fn_decl { + if markused_fn_needs_implicit_veb_ctx(a, tc, cur_module, node) { + enqueue(qualify_fn(cur_module, node.value), mut used, mut queue) + } + } + else {} + } + } +} + +fn markused_fn_needs_implicit_veb_ctx(a &flat.FlatAst, tc &types.TypeChecker, cur_module string, node flat.Node) bool { + return markused_fn_returns_veb_result(tc, node) && markused_fn_has_receiver_param(a, node) + && !markused_fn_receiver_type_is_context(a, node) && !markused_fn_has_param(a, node, 'ctx') + && markused_type_name_known_in_module(tc, cur_module, 'Context') +} + +fn markused_fn_returns_veb_result(tc &types.TypeChecker, node flat.Node) bool { + if node.typ == 'veb.Result' { + return true + } + ret := tc.parse_type(node.typ) + return ret.name() == 'veb.Result' +} + +fn markused_fn_has_receiver_param(a &flat.FlatAst, node flat.Node) bool { + if !node.value.contains('.') || node.children_count == 0 { + return false + } + first := a.child_node(&node, 0) + if first.kind != .param || first.typ.len == 0 { + return false + } + receiver := node.value.all_before_last('.').all_after_last('.') + param_type := first.typ.trim_left('&').all_after_last('.') + return receiver == param_type +} + +fn markused_fn_receiver_type_is_context(a &flat.FlatAst, node flat.Node) bool { + if !markused_fn_has_receiver_param(a, node) { + return false + } + first := a.child_node(&node, 0) + return first.typ.trim_left('&').all_after_last('.') == 'Context' +} + +fn markused_fn_has_param(a &flat.FlatAst, node flat.Node, name string) bool { + for i in 0 .. node.children_count { + param := a.child_node(&node, i) + if param.kind == .param && param.value == name { + return true + } + } + return false +} + +fn markused_type_name_known_in_module(tc &types.TypeChecker, module_name string, typ string) bool { + qtyp := qualify_fn(module_name, typ) + return qtyp in tc.type_aliases || qtyp in tc.structs || qtyp in tc.interface_names + || qtyp in tc.enum_names || qtyp in tc.sum_types +} + fn enqueue_test_file_roots(a &flat.FlatAst, test_files map[string]bool, mut used map[string]bool, mut queue []string) { if test_files.len == 0 { return @@ -781,6 +856,10 @@ fn enqueue_detected_runtime_helpers(a &flat.FlatAst, tc &types.TypeChecker, mut if fn_node.kind == .ident && fn_node.value == 'flag_default_value' { enqueue('escape_default_string', mut used, mut queue) } + if fn_node.kind == .selector + && fn_node.value in ['trim_space', 'trim_space_left', 'trim_space_right', 'to_upper', 'to_upper_ascii', 'to_lower', 'to_lower_ascii'] { + enqueue('string.${fn_node.value}', mut used, mut queue) + } if fn_node.kind == .ident && fn_node.value in ['print', 'println', 'eprint', 'eprintln'] && node.children_count >= 2 { @@ -943,6 +1022,9 @@ fn enqueue_function_value_selectors(a &flat.FlatAst, collector CallCollector, fn if node_idx < ignored_fn_decl_nodes.len && ignored_fn_decl_nodes[node_idx] { continue } + if node_idx < ignored_top_level_nodes.len && ignored_top_level_nodes[node_idx] { + continue + } if node.kind == .ident && node.value.len > 0 { node_id := flat.NodeId(node_idx) is_shadowed := node_idx < shadowed_value_idents.len && shadowed_value_idents[node_idx] @@ -958,9 +1040,6 @@ fn enqueue_function_value_selectors(a &flat.FlatAst, collector CallCollector, fn } continue } - if node_idx < ignored_top_level_nodes.len && ignored_top_level_nodes[node_idx] { - continue - } if node.kind == .selector && node.children_count > 0 && node.value.len > 0 { base_id := a.child(&node, 0) if int(base_id) >= 0 && int(base_id) < shadowed_value_idents.len @@ -1570,7 +1649,11 @@ fn (c &CallCollector) top_level_decl_rhs_type_name(rhs_id flat.NodeId, cur_modul } type_name := resolve_type_name(c.node_type(rhs_id)) if type_name.len > 0 { - return c.struct_lookup_name(type_name, cur_module) + struct_type := c.struct_lookup_name(type_name, cur_module) + if struct_type.len > 0 { + return struct_type + } + return type_name } return '' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 5112403a3..2b1768d77 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -731,7 +731,8 @@ fn (mut t Transformer) transform_all_dispatch(want_parallel bool) bool { // contains a function literal (the only construct that lifts new top-level // declarations and mutates the shared TypeChecker). Collect the remaining, // closure-free functions as parallelizable work items. - pure_items := t.transform_serial_then_collect_pure() + has_fn_literals := t.has_fn_literal_nodes() + pure_items := t.transform_serial_then_collect_pure(has_fn_literals) base_nodes := t.a.nodes.len base_children := t.a.children.len was_parallel := t.run_parallel_transform(pure_items, base_nodes, base_children) @@ -741,10 +742,19 @@ fn (mut t Transformer) transform_all_dispatch(want_parallel bool) bool { return was_parallel } +fn (t &Transformer) has_fn_literal_nodes() bool { + for node in t.a.nodes { + if node.kind == .fn_literal || node.kind == .lambda_expr { + return true + } + } + return false +} + // transform_serial_then_collect_pure walks the top level once: it transforms // const/global declarations and closure-bearing functions in place (serially), // and returns work items for the closure-free functions left to transform. -fn (mut t Transformer) transform_serial_then_collect_pure() []FnWorkItem { +fn (mut t Transformer) transform_serial_then_collect_pure(scan_fn_literals bool) []FnWorkItem { mut pure := []FnWorkItem{} original_len := t.a.nodes.len for i in 0 .. original_len { @@ -758,7 +768,11 @@ fn (mut t Transformer) transform_serial_then_collect_pure() []FnWorkItem { if !t.should_transform_fn(node) { continue } - has_literal, cost := t.fn_subtree_scan(i) + mut has_literal := false + mut cost := int(node.children_count) + 1 + if scan_fn_literals { + has_literal, cost = t.fn_subtree_scan(i) + } if has_literal { t.transform_fn_body(i) } else { diff --git a/vlib/v3/transform/transform_d_parallel.v b/vlib/v3/transform/transform_d_parallel.v index e499171f3..9ca94957c 100644 --- a/vlib/v3/transform/transform_d_parallel.v +++ b/vlib/v3/transform/transform_d_parallel.v @@ -1,6 +1,7 @@ module transform import runtime +import v3.flat // Parallel function-body transform. Compiled only when the build defines // `parallel` (i.e. `-d parallel`), matching the parallel C codegen path. Each @@ -9,7 +10,7 @@ import runtime // fixed order so the build stays deterministic. const min_parallel_transform_items = 256 -const max_parallel_transform_jobs = 4 +const max_parallel_transform_jobs = 2 $if !windows { // TransformChunkArgs is the payload handed to each worker thread. @@ -71,8 +72,10 @@ fn (mut t Transformer) run_parallel_transform(items []FnWorkItem, base_nodes int // which would otherwise block in join, doing useful work. thread_count := chunk_count - 1 mut workers := []voidptr{cap: thread_count} + mut worker_asts := []voidptr{cap: thread_count} for _ in 0 .. thread_count { wast := t.clone_ast_base(base_nodes, base_children) + worker_asts << voidptr(wast) wtc := t.tc.fork_for_parallel_transform(wast) ww := t.fork_worker(wast, wtc) workers << voidptr(ww) @@ -111,6 +114,12 @@ fn (mut t Transformer) run_parallel_transform(items []FnWorkItem, base_nodes int for ci in 0 .. thread_count { ww := unsafe { &Transformer(workers[ci]) } t.merge_worker(ww, chunks[ci + 1], base_nodes, base_children) + // The worker's cloned base AST is no longer needed after merge. + unsafe { + mut wast := &flat.FlatAst(worker_asts[ci]) + wast.nodes.free() + wast.children.free() + } } return true } diff --git a/vlib/v3/types/checker.v b/vlib/v3/types/checker.v index 6f536e720..bc93db0be 100644 --- a/vlib/v3/types/checker.v +++ b/vlib/v3/types/checker.v @@ -349,46 +349,30 @@ fn (mut tc TypeChecker) extend_node_caches(n int) { && n <= tc.expr_type_values.len && n <= tc.checking_nodes.len { return } - old_resolved_call_names := tc.resolved_call_names.clone() - old_resolved_call_set := tc.resolved_call_set.clone() - old_resolved_fn_value_names := tc.resolved_fn_value_names.clone() - old_resolved_fn_value_set := tc.resolved_fn_value_set.clone() - old_expr_type_values := tc.expr_type_values.clone() - old_expr_type_set := tc.expr_type_set.clone() - old_checking_nodes := tc.checking_nodes.clone() - tc.resolved_call_names = []string{len: n} - tc.resolved_call_set = []bool{len: n} - tc.resolved_fn_value_names = []string{len: n} - tc.resolved_fn_value_set = []bool{len: n} - tc.expr_type_values = []Type{len: n, init: Type(void_)} - tc.expr_type_set = []bool{len: n} - tc.checking_nodes = []bool{len: n} - for i in 0 .. old_resolved_call_names.len { - if i >= n { - break - } - tc.resolved_call_names[i] = old_resolved_call_names[i] - tc.resolved_call_set[i] = old_resolved_call_set[i] - } - for i in 0 .. old_resolved_fn_value_names.len { - if i >= n { - break - } - tc.resolved_fn_value_names[i] = old_resolved_fn_value_names[i] - tc.resolved_fn_value_set[i] = old_resolved_fn_value_set[i] + extend_string_cache(mut tc.resolved_call_names, n) + extend_bool_cache(mut tc.resolved_call_set, n) + extend_string_cache(mut tc.resolved_fn_value_names, n) + extend_bool_cache(mut tc.resolved_fn_value_set, n) + extend_type_cache(mut tc.expr_type_values, n) + extend_bool_cache(mut tc.expr_type_set, n) + extend_bool_cache(mut tc.checking_nodes, n) +} + +fn extend_string_cache(mut values []string, n int) { + if n > values.len { + values << []string{len: n - values.len} } - for i in 0 .. old_expr_type_values.len { - if i >= n { - break - } - tc.expr_type_values[i] = old_expr_type_values[i] - tc.expr_type_set[i] = old_expr_type_set[i] +} + +fn extend_bool_cache(mut values []bool, n int) { + if n > values.len { + values << []bool{len: n - values.len} } - for i in 0 .. old_checking_nodes.len { - if i >= n { - break - } - tc.checking_nodes[i] = old_checking_nodes[i] +} + +fn extend_type_cache(mut values []Type, n int) { + if n > values.len { + values << []Type{len: n - values.len, init: Type(void_)} } } @@ -1188,6 +1172,13 @@ fn (mut tc TypeChecker) register_c_variadic_fn(name string) { // visible after its block ends), which is harmless for type lookup since variable // names are effectively unique within a function. pub fn (mut tc TypeChecker) annotate_types() { + tc.annotate_types_with_used(map[string]bool{}) +} + +// annotate_types_with_used annotates only functions that can be emitted when +// `used_fns` is non-empty. This mirrors transform/cgen pruning and avoids +// resolving types in dead, untransformed function bodies after markused. +pub fn (mut tc TypeChecker) annotate_types_with_used(used_fns map[string]bool) { tc.extend_node_caches(tc.a.nodes.len) tc.cur_module = '' for node in tc.a.nodes { @@ -1196,6 +1187,9 @@ pub fn (mut tc TypeChecker) annotate_types() { } else if node.kind == .module_decl { tc.enter_module(node.value) } else if node.kind == .fn_decl { + if !tc.should_annotate_fn(node, used_fns) { + continue + } tc.cur_scope = tc.file_scope tc.push_scope() for pi in 0 .. node.children_count { @@ -1216,6 +1210,34 @@ pub fn (mut tc TypeChecker) annotate_types() { } } +fn (tc &TypeChecker) should_annotate_fn(node flat.Node, used_fns map[string]bool) bool { + if used_fns.len == 0 { + return true + } + qname := checker_qualified_fn_name(tc.cur_module, node.value) + if qname in tc.a.export_fn_names || tc.fn_needs_implicit_veb_ctx(node) { + return true + } + if node.value in used_fns { + return true + } + if qname in used_fns { + return true + } + cname := c_name(qname) + if cname != qname && cname in used_fns { + return true + } + return false +} + +fn checker_qualified_fn_name(mod string, name string) string { + if mod.len == 0 || mod == 'main' || mod == 'builtin' { + return name + } + return '${mod}.${name}' +} + // annotate_node supports annotate node handling for TypeChecker. fn (mut tc TypeChecker) annotate_node(id flat.NodeId) { if int(id) < 0 { @@ -1536,7 +1558,7 @@ fn (mut tc TypeChecker) remember_resolved_call(id flat.NodeId, name string) { return } if idx >= tc.resolved_call_names.len { - tc.reset_node_caches(tc.a.nodes.len) + tc.extend_node_caches(tc.a.nodes.len) } if idx < tc.resolved_call_names.len { tc.resolved_call_names[idx] = name @@ -1551,7 +1573,7 @@ fn (mut tc TypeChecker) remember_resolved_fn_value(id flat.NodeId, name string) return } if idx >= tc.resolved_fn_value_names.len { - tc.reset_node_caches(tc.a.nodes.len) + tc.extend_node_caches(tc.a.nodes.len) } if idx < tc.resolved_fn_value_names.len { tc.resolved_fn_value_names[idx] = name @@ -1584,7 +1606,7 @@ fn (mut tc TypeChecker) remember_expr_type(id flat.NodeId, typ Type) { if should_cache_expr_type(kind, typ) { idx := int(id) if idx >= tc.expr_type_values.len { - tc.reset_node_caches(tc.a.nodes.len) + tc.extend_node_caches(tc.a.nodes.len) } if idx < tc.expr_type_values.len { tc.expr_type_values[idx] = typ @@ -2873,7 +2895,7 @@ fn (mut tc TypeChecker) check_node(id flat.NodeId) { return } if idx >= tc.checking_nodes.len { - tc.reset_node_caches(tc.a.nodes.len) + tc.extend_node_caches(tc.a.nodes.len) } if idx < tc.checking_nodes.len { if tc.checking_nodes[idx] { diff --git a/vlib/v3/v3.v b/vlib/v3/v3.v index 454362800..820f83347 100644 --- a/vlib/v3/v3.v +++ b/vlib/v3/v3.v @@ -365,7 +365,7 @@ fn main() { pre_tc.reject_unlowered_map_mutation = true set_diagnostic_files(mut pre_tc, user_files) set_unsupported_generic_files(mut pre_tc, a, is_selfhost, diagnostic_root) - pre_tc.annotate_types() + pre_tc.annotate_types_with_used(used_fns) b.step('annotate types') if backend == 'wasm' { -- 2.39.5