From 057970b7fc13981239914ab17b890ab27ce24a95 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 24 Jun 2026 19:46:17 +0300 Subject: [PATCH] v2: parallel transformer (x2 sped up) --- vlib/v3/flat/flat.v | 18 ++ vlib/v3/gen/c/cleanc.v | 7 + vlib/v3/gen/c/fn.v | 108 +++++++--- vlib/v3/gen/c/fn_d_parallel.v | 1 + vlib/v3/transform/transform.v | 217 +++++++++++++++++++- vlib/v3/transform/transform_d_parallel.v | 119 +++++++++++ vlib/v3/transform/transform_notd_parallel.v | 9 + vlib/v3/types/checker.v | 23 +++ vlib/v3/v3.v | 14 +- 9 files changed, 486 insertions(+), 30 deletions(-) create mode 100644 vlib/v3/transform/transform_d_parallel.v create mode 100644 vlib/v3/transform/transform_notd_parallel.v diff --git a/vlib/v3/flat/flat.v b/vlib/v3/flat/flat.v index 655b99f00..a486b1c9b 100644 --- a/vlib/v3/flat/flat.v +++ b/vlib/v3/flat/flat.v @@ -220,6 +220,24 @@ pub fn (mut a FlatAst) add_val_id(kind_id int, value string) NodeId { return id } +// with_shifted_children returns a copy of the node whose children_start is moved +// by `shift`. children_start lives in the immutable section of Node, so callers +// that relocate a node's children block (e.g. the parallel-transform merge) build +// a fresh node instead of mutating in place. +pub fn (n Node) with_shifted_children(shift i32) Node { + return Node{ + value: n.value + typ: n.typ + generic_params: n.generic_params + kind_id: n.kind_id + pos: n.pos + children_start: n.children_start + shift + children_count: n.children_count + kind: n.kind + op: n.op + } +} + // add_node updates add node state for FlatAst. pub fn (mut a FlatAst) add_node(node Node) NodeId { id := NodeId(a.nodes.len) diff --git a/vlib/v3/gen/c/cleanc.v b/vlib/v3/gen/c/cleanc.v index 9158ee9b1..2188e207b 100644 --- a/vlib/v3/gen/c/cleanc.v +++ b/vlib/v3/gen/c/cleanc.v @@ -63,6 +63,7 @@ mut: array_method_cache map[string]string 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) + param_types_by_short map[string][]types.Type // method short-name suffix -> param types (fallback index) spawn_wrapper_names map[string]string spawn_wrapper_defs []string parallel_used bool @@ -119,6 +120,7 @@ pub fn FlatGen.new() FlatGen { array_method_cache: map[string]string{} param_types_cache: map[string][]types.Type{} embedded_fields_by_type: map[string][]types.StructField{} + param_types_by_short: map[string][]types.Type{} spawn_wrapper_names: map[string]string{} spawn_wrapper_defs: []string{} str_lits: []string{} @@ -189,6 +191,7 @@ pub fn (mut g FlatGen) gen_with_used_options(a &flat.FlatAst, used_fns map[strin 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.param_types_by_short = map[string][]types.Type{} g.spawn_wrapper_names = map[string]string{} g.spawn_wrapper_defs = []string{} g.parallel_used = false @@ -199,6 +202,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.precompute_param_type_index() g.collect_interface_impls() g.preseed_struct_fn_ptr_types() g.preseed_global_fn_ptr_types() @@ -215,6 +219,9 @@ pub fn (mut g FlatGen) gen_with_used_options(a &flat.FlatAst, used_fns map[strin g.enum_decls() g.type_alias_decls() g.type_forward_decls() + // Forward-declare multi-return structs before fn-ptr typedefs, which may name a + // multi-return as a by-value return type (full bodies come after struct_decls). + g.multi_return_forward_decls() g.fn_ptr_typedefs() g.struct_decls() g.fixed_array_typedefs() diff --git a/vlib/v3/gen/c/fn.v b/vlib/v3/gen/c/fn.v index 43835ffe5..33f9094b4 100644 --- a/vlib/v3/gen/c/fn.v +++ b/vlib/v3/gen/c/fn.v @@ -2044,22 +2044,36 @@ fn (mut g FlatGen) param_types_for_uncached(name string, fallback string) []type return decl_types } if name.contains('.') { + // O(1) lookup via the precomputed short-name index instead of scanning the whole + // function table on every (cache-missing) call — this fallback ran ~3000× and was + // a top cgen self-time cost (each scan is O(functions)). short_name := name.all_after_last('.') - suffix := '.${short_name}' - // Look up the value only on a match: `for _, v in map` copies every array value - // on every iteration, which is wasteful for a table-wide scan. - for candidate, _ in g.fn_decl_param_types { - if candidate.ends_with(suffix) { - return g.fn_decl_param_types[candidate] + if params := g.param_types_by_short[short_name] { + return params + } + } + return []types.Type{} +} + +// precompute_param_type_index builds short-name -> param-types, preserving the fallback's +// priority (fn_decl_param_types first, then the checker's fn_param_types; first match wins). +fn (mut g FlatGen) precompute_param_type_index() { + for name, params in g.fn_decl_param_types { + if name.contains('.') { + short := name.all_after_last('.') + if short !in g.param_types_by_short { + g.param_types_by_short[short] = params } } - for candidate, _ in g.tc.fn_param_types { - if candidate.ends_with(suffix) { - return g.tc.fn_param_types[candidate] + } + for name, params in g.tc.fn_param_types { + if name.contains('.') { + short := name.all_after_last('.') + if short !in g.param_types_by_short { + g.param_types_by_short[short] = params } } } - return []types.Type{} } fn (g &FlatGen) interface_method_param_types(name string) ?[]types.Type { @@ -3051,11 +3065,46 @@ fn fn_ptr_typedef_is_generic_placeholder(typ string) bool { return short.len == 1 && short[0] >= `A` && short[0] <= `Z` } -// multi_return_typedefs supports multi return typedefs handling for FlatGen. +// multi_return_forward_decls forward-declares every multi-return struct that the +// generated C can reference by name. It must run before fn_ptr_typedefs(), because +// a function-pointer typedef may name a multi-return as its (by-value) return type +// — and a `typedef RET (*fp)(...)` only needs RET's tag declared, not its full +// layout. The full struct bodies are emitted later by multi_return_typedefs(), +// after the member struct definitions they depend on are available. +fn (mut g FlatGen) multi_return_forward_decls() { + mut emitted := map[string]bool{} + g.walk_multi_return_typedefs(mut emitted, true) + // Also cover multi-returns reachable only as a fn-pointer return type: parallel + // cgen preseeds fn-ptr types that the serial path never materializes, so their + // return multi-returns may not appear among the function/expression types above. + for encoded, _ in g.fn_ptr_types { + ret, _ := fn_ptr_typedef_parts(encoded) + if ret.starts_with('multi_return_') && ret !in emitted { + emitted[ret] = true + g.writeln('typedef struct ${ret} ${ret};') + } + } + if emitted.len > 0 { + g.writeln('') + } +} + +// multi_return_typedefs emits the full struct definitions for multi-return types. fn (mut g FlatGen) multi_return_typedefs() { mut emitted := map[string]bool{} + g.walk_multi_return_typedefs(mut emitted, false) + if emitted.len > 0 { + g.writeln('') + } +} + +// walk_multi_return_typedefs visits every multi-return type reachable from a +// function return type or an expression type and emits it via emit_multi_return_typedef. +// Shared by the forward-declaration and full-definition passes so both see the same +// set in the same (deterministic) order. +fn (mut g FlatGen) walk_multi_return_typedefs(mut emitted map[string]bool, forward_only bool) { for _, ret in g.tc.fn_ret_types { - g.emit_multi_return_typedef(ret, mut emitted) + g.emit_multi_return_typedef(ret, mut emitted, forward_only) } mut cur_module := '' mut cur_file := '' @@ -3076,23 +3125,28 @@ fn (mut g FlatGen) multi_return_typedefs() { } g.tc.cur_file = cur_file g.tc.cur_module = cur_module - if node.typ.len > 0 { - g.emit_multi_return_typedef(g.tc.parse_type(node.typ), mut emitted) + // emit_multi_return_typedef only acts on (optionally `?`/`!`-wrapped) multi-return + // types, whose string form always begins with `(`. Skip parse_type for everything + // else — this ran on every node's type (~hundreds of thousands of parse_type calls). + typ := node.typ + if typ.len > 0 && (typ[0] == `(` || ((typ[0] == `?` || typ[0] == `!`) && typ.len > 1 + && typ[1] == `(`)) { + g.emit_multi_return_typedef(g.tc.parse_type(typ), mut emitted, forward_only) } } - if emitted.len > 0 { - g.writeln('') - } } -// emit_multi_return_typedef emits emit multi return typedef output for c. -fn (mut g FlatGen) emit_multi_return_typedef(ret types.Type, mut emitted map[string]bool) { +// emit_multi_return_typedef emits one multi-return type: a forward declaration +// (`typedef struct NAME NAME;`) when forward_only, otherwise the full struct body +// (`struct NAME { ... };`). The two forms are paired — the forward decl provides the +// typedef name, the body completes the tagged struct. +fn (mut g FlatGen) emit_multi_return_typedef(ret types.Type, mut emitted map[string]bool, forward_only bool) { if ret is types.OptionType { - g.emit_multi_return_typedef(ret.base_type, mut emitted) + g.emit_multi_return_typedef(ret.base_type, mut emitted, forward_only) return } if ret is types.ResultType { - g.emit_multi_return_typedef(ret.base_type, mut emitted) + g.emit_multi_return_typedef(ret.base_type, mut emitted, forward_only) return } if ret is types.MultiReturn { @@ -3101,11 +3155,15 @@ fn (mut g FlatGen) emit_multi_return_typedef(ret types.Type, mut emitted map[str return } emitted[name] = true - g.writeln('typedef struct {') - for i, typ in ret.types { - g.writeln('\t${g.tc.c_type(typ)} arg${i};') + if forward_only { + g.writeln('typedef struct ${name} ${name};') + } else { + g.writeln('struct ${name} {') + for i, typ in ret.types { + g.writeln('\t${g.tc.c_type(typ)} arg${i};') + } + g.writeln('};') } - g.writeln('} ${name};') } } diff --git a/vlib/v3/gen/c/fn_d_parallel.v b/vlib/v3/gen/c/fn_d_parallel.v index 0c1872fe5..b9dffc083 100644 --- a/vlib/v3/gen/c/fn_d_parallel.v +++ b/vlib/v3/gen/c/fn_d_parallel.v @@ -373,6 +373,7 @@ fn (g &FlatGen) new_parallel_worker(worker_id int) &FlatGen { 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() + param_types_by_short: g.param_types_by_short.clone() } } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 3bc1d03f4..5dae45966 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -140,6 +140,15 @@ pub fn transform(mut a flat.FlatAst, tc &types.TypeChecker) { // transform_with_used transforms transform with used data for transform. pub fn transform_with_used(mut a flat.FlatAst, tc &types.TypeChecker, used_fns map[string]bool) map[string]bool { + augmented, _ := transform_with_used_opt(mut a, tc, used_fns, false) + return augmented +} + +// transform_with_used_opt is transform_with_used with an opt-in for parallel +// function-body transform. It returns the augmented used-fn set and whether the +// function bodies were actually transformed across threads (false when parallel +// was not requested, the build lacks thread support, or there was too little work). +pub fn transform_with_used_opt(mut a flat.FlatAst, tc &types.TypeChecker, used_fns map[string]bool, want_parallel bool) (map[string]bool, bool) { mut augmented_used_fns := used_fns.clone() mut t := new_transformer(mut a, tc, augmented_used_fns) t.prepare() @@ -155,8 +164,8 @@ pub fn transform_with_used(mut a flat.FlatAst, tc &types.TypeChecker, used_fns m if reserve_children > 0 { unsafe { a.children.grow_cap(reserve_children) } } - t.transform_all() - return augmented_used_fns + was_parallel := t.transform_all_dispatch(want_parallel) + return augmented_used_fns, was_parallel } pub fn monomorphize_with_used(mut a flat.FlatAst, tc &types.TypeChecker, used_fns map[string]bool) map[string]bool { @@ -537,6 +546,210 @@ fn (mut t Transformer) transform_all() { } } +// FnWorkItem identifies one top-level function body to transform, together with +// the file/module context active at its declaration and a rough cost estimate +// (subtree node count) used to balance work across parallel workers. +struct FnWorkItem { + fn_idx int + file string + module string + cost int +} + +// transform_all_dispatch runs the main transform pass either serially (the +// original single-threaded walk) or, when `want_parallel` is set and there is +// enough work, with closure-free function bodies transformed across threads. +// Returns whether function bodies were actually transformed in parallel. +fn (mut t Transformer) transform_all_dispatch(want_parallel bool) bool { + if !want_parallel { + t.transform_all() + return false + } + // Serial phase: transform consts/globals and every function whose body + // 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() + base_nodes := t.a.nodes.len + base_children := t.a.children.len + return t.run_parallel_transform(pure_items, base_nodes, base_children) +} + +// 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 { + mut pure := []FnWorkItem{} + original_len := t.a.nodes.len + for i in 0 .. original_len { + node := t.a.nodes[i] + kind_id := node_kind_id(node) + if kind_id == 77 { + t.cur_file = node.value + } else if kind_id == 73 { + t.cur_module = node.value + } else if kind_id == 61 { + if !t.should_transform_fn(node) { + continue + } + has_literal, cost := t.fn_subtree_scan(i) + if has_literal { + t.transform_fn_body(i) + } else { + pure << FnWorkItem{ + fn_idx: i + file: t.cur_file + module: t.cur_module + cost: cost + } + } + } else if kind_id == 65 { + t.transform_const_decl(node) + } else if kind_id == 64 { + t.transform_global_decl(node) + } + } + return pure +} + +// fn_subtree_scan walks the subtree rooted at the function declaration `fn_idx` +// and reports (whether it contains a function literal, total node count). The +// function-literal flag routes closure-bearing functions to the serial path; the +// count is a cheap per-function cost used to balance parallel workers. +fn (t &Transformer) fn_subtree_scan(fn_idx int) (bool, int) { + mut has_literal := false + mut count := 0 + mut stack := [flat.NodeId(fn_idx)] + for stack.len > 0 { + id := stack.pop() + idx := int(id) + if idx < 0 || idx >= t.a.nodes.len { + continue + } + node := t.a.nodes[idx] + count++ + // 21 = fn_literal, 32 = lambda_expr: both can lower to a lifted closure. + kid := node_kind_id(node) + if kid == 21 || kid == 32 { + has_literal = true + } + for ci in 0 .. node.children_count { + child_id := t.a.children[node.children_start + ci] + if int(child_id) >= 0 { + stack << child_id + } + } + } + return has_literal, count +} + +// transform_pure_items_serial transforms a list of closure-free function bodies +// on this Transformer, in order. Used both as the serial fallback and as the +// per-worker body in the parallel path. +fn (mut t Transformer) transform_pure_items_serial(items []FnWorkItem) { + for it in items { + t.cur_file = it.file + t.cur_module = it.module + t.transform_fn_body(it.fn_idx) + } +} + +// clone_ast_base produces a private FlatAst holding an independent copy of the +// first base_nodes nodes / base_children children, so a worker can append its own +// transformed nodes without racing the master or other workers. Read-only metadata +// (disabled_fns) is shared. +fn (t &Transformer) clone_ast_base(base_nodes int, base_children int) &flat.FlatAst { + return &flat.FlatAst{ + nodes: t.a.nodes[0..base_nodes].clone() + children: t.a.children[0..base_children].clone() + user_code_start: t.a.user_code_start + disabled_fns: t.a.disabled_fns + } +} + +// fork_worker builds a worker Transformer that shares this transformer's +// read-only collected maps (structs, sum types, fn return types, used_fns, …) +// and operates on its own cloned AST `ast` and forked TypeChecker `wtc`. All +// per-function mutable state and memoization caches are reset/private so the +// worker can run on its own thread. +fn (t &Transformer) fork_worker(ast &flat.FlatAst, wtc &types.TypeChecker) &Transformer { + mut w := *t + w.a = ast + w.tc = wtc + w.alias_cache = &AliasCache{} + w.sum_cache = &AliasCache{} + w.var_types = []VarTypeBinding{} + w.smartcast_stack = []SmartcastContext{} + w.pending_stmts = []flat.NodeId{} + w.pointer_value_lvalues = map[string]bool{} + w.temp_counter = 0 + w.cur_file = '' + w.cur_module = '' + w.cur_fn_name = '' + w.cur_fn_ret_type = '' + w.in_call_callee = false + w.in_const_init = false + w.in_return_expr = false + return &w +} + +// merge_worker folds a finished worker's transformed output back into the master +// AST. The worker created its new nodes/children at indices base_nodes/base_children +// (matching the master at fork time); here they are appended to the master and every +// reference to a worker-local new node or new children block is shifted by the +// distance the block moved. `items` lists the function indices this worker owned, so +// their rewritten top-level nodes can be copied into place. +fn (mut t Transformer) merge_worker(w &Transformer, items []FnWorkItem, base_nodes int, base_children int) { + node_shift := i32(t.a.nodes.len - base_nodes) + child_shift := i32(t.a.children.len - base_children) + // New children: relocate references to worker-local new nodes. + for k in base_children .. w.a.children.len { + cid := w.a.children[k] + if int(cid) >= base_nodes { + t.a.children << flat.NodeId(int(cid) + int(node_shift)) + } else { + t.a.children << cid + } + } + // New nodes: relocate children_start that points into the new children block. + for k in base_nodes .. w.a.nodes.len { + n := w.a.nodes[k] + if n.children_start >= base_children { + t.a.nodes << n.with_shifted_children(child_shift) + } else { + t.a.nodes << n + } + } + // Rewritten top-level function nodes keep their original index in the master. + for it in items { + n := w.a.nodes[it.fn_idx] + if n.children_start >= base_children { + t.a.nodes[it.fn_idx] = n.with_shifted_children(child_shift) + } else { + t.a.nodes[it.fn_idx] = n + } + } +} + +// split_work_items distributes items across `n` buckets using greedy +// least-loaded-by-cost assignment, so heavy functions are spread evenly. The +// assignment is deterministic for a given input (required for reproducible builds). +fn split_work_items(items []FnWorkItem, n int) [][]FnWorkItem { + mut buckets := [][]FnWorkItem{len: n, init: []FnWorkItem{}} + mut loads := []i64{len: n} + for it in items { + mut best := 0 + for b in 1 .. n { + if loads[b] < loads[best] { + best = b + } + } + buckets[best] << it + loads[best] += i64(it.cost) + 1 + } + return buckets +} + // should_transform_fn reports whether should transform fn applies in transform. fn (t &Transformer) should_transform_fn(node flat.Node) bool { if t.used_fns.len == 0 { diff --git a/vlib/v3/transform/transform_d_parallel.v b/vlib/v3/transform/transform_d_parallel.v new file mode 100644 index 000000000..9f2b45350 --- /dev/null +++ b/vlib/v3/transform/transform_d_parallel.v @@ -0,0 +1,119 @@ +module transform + +import runtime + +// Parallel function-body transform. Compiled only when the build defines +// `parallel` (i.e. `-d parallel`), matching the parallel C codegen path. Each +// worker transforms a disjoint set of closure-free functions on its own cloned +// AST + forked TypeChecker, and the master folds the results back together in a +// fixed order so the build stays deterministic. + +const min_parallel_transform_items = 256 +const max_parallel_transform_jobs = 4 + +$if !windows { + // TransformChunkArgs is the payload handed to each worker thread. + struct TransformChunkArgs { + worker voidptr // &Transformer + items_ptr voidptr // &[]FnWorkItem + } + + // C.pthread_t declares C pthread t data used by transform. + @[typedef] + struct C.pthread_t {} + + // C.pthread_create declares the C pthread_create symbol used by transform. + fn C.pthread_create(thread &C.pthread_t, attr voidptr, start_routine fn (voidptr) voidptr, arg voidptr) int + + // C.pthread_join declares the C pthread_join symbol used by transform. + fn C.pthread_join(thread C.pthread_t, retval voidptr) int + + // C.pthread_attr_init declares the C pthread_attr_init symbol used by transform. + fn C.pthread_attr_init(attr voidptr) int + + // C.pthread_attr_setstacksize declares the C pthread_attr_setstacksize symbol used by transform. + fn C.pthread_attr_setstacksize(attr voidptr, stacksize usize) int + + // C.pthread_attr_destroy declares the C pthread_attr_destroy symbol used by transform. + fn C.pthread_attr_destroy(attr voidptr) int + + // transform_chunk_thread runs one worker's chunk of function bodies. + fn transform_chunk_thread(arg voidptr) voidptr { + a := unsafe { &TransformChunkArgs(arg) } + mut w := unsafe { &Transformer(a.worker) } + items := unsafe { &[]FnWorkItem(a.items_ptr) } + w.transform_pure_items_serial(*items) + return unsafe { nil } + } +} + +// run_parallel_transform transforms the closure-free function bodies across +// threads when there is enough work, otherwise serially. Returns whether threads +// were actually used. +fn (mut t Transformer) run_parallel_transform(items []FnWorkItem, base_nodes int, base_children int) bool { + $if windows { + t.transform_pure_items_serial(items) + return false + } $else { + n_jobs := transform_job_count(runtime.nr_jobs(), items.len) + if items.len < min_parallel_transform_items || n_jobs <= 1 { + t.transform_pure_items_serial(items) + return false + } + mut chunks := split_work_items(items, n_jobs) + chunk_count := chunks.len + + // Build one worker per chunk: a private AST clone + forked TypeChecker. + mut workers := []voidptr{cap: chunk_count} + for _ in 0 .. chunk_count { + wast := t.clone_ast_base(base_nodes, base_children) + wtc := t.tc.fork_for_parallel_transform(wast) + ww := t.fork_worker(wast, wtc) + workers << voidptr(ww) + } + mut args := []TransformChunkArgs{cap: chunk_count} + for ci in 0 .. chunk_count { + args << TransformChunkArgs{ + worker: workers[ci] + items_ptr: unsafe { voidptr(&chunks[ci]) } + } + } + + mut thread_ids := []C.pthread_t{len: chunk_count} + attr_buf := [64]u8{} + attr := unsafe { voidptr(&attr_buf[0]) } + C.pthread_attr_init(attr) + // Transform recurses deeply on large expressions; give workers a roomy stack. + C.pthread_attr_setstacksize(attr, 64 * 1024 * 1024) + for ci in 0 .. chunk_count { + C.pthread_create(unsafe { &thread_ids[ci] }, attr, transform_chunk_thread, + unsafe { voidptr(&args[ci]) }) + } + C.pthread_attr_destroy(attr) + for ci in 0 .. chunk_count { + C.pthread_join(thread_ids[ci], unsafe { nil }) + } + // Merge in a fixed order for reproducible node numbering. + for ci in 0 .. chunk_count { + ww := unsafe { &Transformer(workers[ci]) } + t.merge_worker(ww, chunks[ci], base_nodes, base_children) + } + return true + } +} + +// transform_job_count caps the worker count by both the runtime job count and a +// fixed ceiling (each worker clones the base AST, so more workers cost more memory). +fn transform_job_count(n_runtime_jobs int, n_items int) int { + if n_runtime_jobs <= 0 || n_items <= 0 { + return 0 + } + mut n := n_runtime_jobs + if n > max_parallel_transform_jobs { + n = max_parallel_transform_jobs + } + if n > n_items { + n = n_items + } + return n +} diff --git a/vlib/v3/transform/transform_notd_parallel.v b/vlib/v3/transform/transform_notd_parallel.v new file mode 100644 index 000000000..12a68d019 --- /dev/null +++ b/vlib/v3/transform/transform_notd_parallel.v @@ -0,0 +1,9 @@ +module transform + +// run_parallel_transform is the serial fallback compiled when the build does not +// define `parallel` (no thread support). It transforms the closure-free function +// bodies on the calling thread and reports that no threading happened. +fn (mut t Transformer) run_parallel_transform(items []FnWorkItem, _base_nodes int, _base_children int) bool { + t.transform_pure_items_serial(items) + return false +} diff --git a/vlib/v3/types/checker.v b/vlib/v3/types/checker.v index bf57931fe..1de3d824c 100644 --- a/vlib/v3/types/checker.v +++ b/vlib/v3/types/checker.v @@ -180,6 +180,29 @@ pub fn TypeChecker.new(a &flat.FlatAst) TypeChecker { } } +// fork_for_parallel_transform returns a TypeChecker that shares all of `tc`'s +// read-only data (semantic maps and node-indexed cache arrays, which the transform +// pass only reads) but owns a fresh, private `type_cache` and a private AST view. +// During transform the only hidden mutation a TypeChecker performs through its `&` +// receiver is memoization into `type_cache` (parse_type / c_type); giving each +// worker its own cache makes concurrent use race-free without cloning the large +// semantic maps. `ast` must be the worker's own (cloned) FlatAst so that any +// expr_type lookup on a freshly-created node id indexes a valid array. +pub fn (tc &TypeChecker) fork_for_parallel_transform(ast &flat.FlatAst) &TypeChecker { + mut forked := *tc + forked.a = ast + forked.type_cache = &TypeCache{ + parse_enabled: if tc.type_cache != unsafe { nil } { + tc.type_cache.parse_enabled + } else { + false + } + parse_entries: map[string]Type{} + c_entries: map[string]string{} + } + return &forked +} + // reset_node_caches updates reset node caches state for types. fn (mut tc TypeChecker) reset_node_caches(n int) { tc.resolved_call_names = []string{len: n} diff --git a/vlib/v3/v3.v b/vlib/v3/v3.v index 9fd7a9f5c..2ffbda854 100644 --- a/vlib/v3/v3.v +++ b/vlib/v3/v3.v @@ -131,6 +131,7 @@ fn main() { mut is_strict := false mut is_selfhost := false mut no_parallel := false + mut parallel_transform := false mut building_v := false mut user_defines := []string{} mut i := 0 @@ -158,6 +159,9 @@ fn main() { } else if args[i] == '-no-parallel' || args[i] == '--no-parallel' { no_parallel = true i++ + } else if args[i] == '-parallel-transform' || args[i] == '--parallel-transform' { + parallel_transform = true + i++ } else if args[i] == '-d' && i + 1 < args.len { user_defines << args[i + 1] i += 2 @@ -279,9 +283,13 @@ fn main() { mut used_fns := markused.mark_used(a, pre_tc) b.step('markused') - // Transform (match lowering, string/in lowering, etc.) - used_fns = transform.transform_with_used(mut a, &pre_tc, used_fns) - b.step('transform') + // Transform (match lowering, string/in lowering, etc.). Parallel transform is an + // explicit opt-in (`-parallel-transform`), independent of `-no-parallel` (which + // gates the parallel C codegen): the two phases can be threaded independently. + mut transform_was_parallel := false + used_fns, transform_was_parallel = transform.transform_with_used_opt(mut a, &pre_tc, used_fns, + parallel_transform) + b.step_parallel('transform', transform_was_parallel) // Reuse the pre-transform checker for metadata only. Transform does not add // declarations, and v1/v2 do not run a second semantic checker after lowering. -- 2.39.5