Skip to content

Commit abb03c7

Browse files
committed
v3: parallel optimizations
1 parent d480353 commit abb03c7

5 files changed

Lines changed: 130 additions & 79 deletions

File tree

vlib/v3/gen/c/fn_d_parallel.v

Lines changed: 101 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -73,34 +73,48 @@ fn (mut g FlatGen) gen_fns_dispatch(no_parallel bool) {
7373
g.prepare_parallel_items(items)
7474
mut chunk_items := split_flat_cgen_items(items, n_jobs)
7575
chunk_count := chunk_items.len
76-
mut thread_ids := []C.pthread_t{len: chunk_count}
77-
mut args := []FlatCgenChunkArgs{cap: chunk_count}
78-
mut workers := []voidptr{cap: chunk_count}
76+
// chunk[0] is emitted by the master (this thread) directly into its own builder
77+
// — no worker clone. Only chunks[1..] get helper threads with a cloned FlatGen.
78+
// This drops one full FlatGen clone from the peak and uses the master thread that
79+
// would otherwise just block in join.
80+
thread_count := chunk_count - 1
81+
mut thread_ids := []C.pthread_t{len: thread_count}
82+
mut args := []FlatCgenChunkArgs{cap: thread_count}
83+
mut workers := []voidptr{cap: thread_count}
7984

80-
for ci := 0; ci < chunk_count; ci++ {
81-
w := g.new_parallel_worker(ci)
85+
// Helper workers keep the same temp-name base they had when every chunk was a
86+
// worker (worker_id ci+1 -> base (ci+2)*100_000), and the master adopts what was
87+
// worker 0's base. This keeps each chunk in its own disjoint _tN range AND makes
88+
// the output byte-identical to the all-workers version.
89+
for ci := 0; ci < thread_count; ci++ {
90+
w := g.new_parallel_worker(ci + 1)
8291
workers << voidptr(w)
8392
}
84-
for ci := 0; ci < chunk_count; ci++ {
93+
for ci := 0; ci < thread_count; ci++ {
8594
args << FlatCgenChunkArgs{
8695
worker: workers[ci]
87-
work_items_ptr: unsafe { voidptr(&chunk_items[ci]) }
96+
work_items_ptr: unsafe { voidptr(&chunk_items[ci + 1]) }
8897
}
8998
}
9099

91100
attr_buf := [64]u8{}
92101
attr := unsafe { voidptr(&attr_buf[0]) }
93102
C.pthread_attr_init(attr)
94103
C.pthread_attr_setstacksize(attr, 64 * 1024 * 1024)
95-
for ci := 0; ci < chunk_count; ci++ {
104+
for ci := 0; ci < thread_count; ci++ {
96105
C.pthread_create(unsafe { &thread_ids[ci] }, attr, flat_cgen_chunk_thread,
97106
unsafe { voidptr(&args[ci]) })
98107
}
99108
C.pthread_attr_destroy(attr)
100-
for ci := 0; ci < chunk_count; ci++ {
109+
// Master emits chunk[0] into g.sb while the helper threads run, using worker 0's
110+
// old temp base so its emitted temps match the all-workers numbering.
111+
g.tmp_count = 100_000
112+
g.gen_fn_items(chunk_items[0])
113+
for ci := 0; ci < thread_count; ci++ {
101114
C.pthread_join(thread_ids[ci], unsafe { nil })
102115
}
103-
for ci := 0; ci < chunk_count; ci++ {
116+
// Merge helper output after the master's chunk[0], in fixed order.
117+
for ci := 0; ci < thread_count; ci++ {
104118
w := unsafe { &FlatGen(workers[ci]) }
105119
g.merge_parallel_worker(w)
106120
}
@@ -324,39 +338,50 @@ fn (mut g FlatGen) fn_ptr_type_key(typ types.FnType) string {
324338
return 'fn_ptr:${ret}|${params.join(', ')}'
325339
}
326340

327-
// new_parallel_worker supports new parallel worker handling for FlatGen.
341+
// new_parallel_worker builds a per-worker FlatGen for parallel codegen.
342+
//
343+
// The lookup tables populated before gen_fns_dispatch (in collect_gen_info,
344+
// collect_interface_impls and the precompute_* passes) are READ-ONLY during codegen, so
345+
// they are SHARED by reference instead of cloned — V maps/arrays are reference types and
346+
// concurrent readers are safe. Only the state a worker actually mutates while emitting is
347+
// kept private: the output builder; the string-literal table (interned during gen); the
348+
// fn_ptr_types / needed_optional_types / emitted_* sets and the param_types_cache /
349+
// array_method_cache memoization caches (all written during gen); the per-function
350+
// cur_param_* scratch; and runtime_inits (kept private out of caution). This drops the
351+
// bulk of each worker's clone cost — previously the whole table set was duplicated per
352+
// worker and, under -gc none, never freed.
328353
fn (g &FlatGen) new_parallel_worker(worker_id int) &FlatGen {
329354
return &FlatGen{
330355
sb: strings.new_builder(64_000)
331356
a: unsafe { g.a }
332-
used_fns: g.used_fns.clone()
333-
used_fn_names: g.used_fn_names.clone()
357+
used_fns: g.used_fns
358+
used_fn_names: g.used_fn_names
334359
str_lits: g.str_lits.clone()
335360
str_lit_ids: g.str_lit_ids.clone()
336-
global_types: g.global_types.clone()
337-
enum_vals: g.enum_vals.clone()
338-
interfaces: g.interfaces.clone()
339-
const_vals: g.const_vals.clone()
340-
const_modules: g.const_modules.clone()
341-
const_init_order: g.const_init_order.clone()
342-
global_modules: g.global_modules.clone()
343-
global_inits: g.global_inits.clone()
344-
global_init_order: g.global_init_order.clone()
345-
iface_impls: g.iface_impls.clone()
346-
iface_type_ids: g.iface_type_ids.clone()
347-
module_init_fns: g.module_init_fns.clone()
348-
module_init_fn_modules: g.module_init_fn_modules.clone()
349-
module_imports: g.module_imports.clone()
361+
global_types: g.global_types
362+
enum_vals: g.enum_vals
363+
interfaces: g.interfaces
364+
const_vals: g.const_vals
365+
const_modules: g.const_modules
366+
const_init_order: g.const_init_order
367+
global_modules: g.global_modules
368+
global_inits: g.global_inits
369+
global_init_order: g.global_init_order
370+
iface_impls: g.iface_impls
371+
iface_type_ids: g.iface_type_ids
372+
module_init_fns: g.module_init_fns
373+
module_init_fn_modules: g.module_init_fn_modules
374+
module_imports: g.module_imports
350375
tc: g.clone_parallel_type_checker()
351376
has_builtins: g.has_builtins
352377
tmp_count: (worker_id + 1) * 100_000
353378
line_start: true
354-
modules: g.modules.clone()
379+
modules: g.modules
355380
fn_ptr_types: g.fn_ptr_types.clone()
356-
fn_decl_param_types: g.fn_decl_param_types.clone()
357-
fn_decl_ret_types: g.fn_decl_ret_types.clone()
358-
struct_decl_infos: g.struct_decl_infos.clone()
359-
struct_decl_short_infos: g.struct_decl_short_infos.clone()
381+
fn_decl_param_types: g.fn_decl_param_types
382+
fn_decl_ret_types: g.fn_decl_ret_types
383+
struct_decl_infos: g.struct_decl_infos
384+
struct_decl_short_infos: g.struct_decl_short_infos
360385
runtime_inits: g.runtime_inits.clone()
361386
compiler_vroot: g.compiler_vroot
362387
cur_param_names: g.cur_param_names.clone()
@@ -372,57 +397,69 @@ fn (g &FlatGen) new_parallel_worker(worker_id int) &FlatGen {
372397
emitted_fns: g.emitted_fns.clone()
373398
array_method_cache: g.array_method_cache.clone()
374399
param_types_cache: g.param_types_cache.clone()
375-
embedded_fields_by_type: g.embedded_fields_by_type.clone()
376-
param_types_by_short: g.param_types_by_short.clone()
400+
embedded_fields_by_type: g.embedded_fields_by_type
401+
param_types_by_short: g.param_types_by_short
377402
}
378403
}
379404

380-
// clone_parallel_type_checker supports clone parallel type checker handling for FlatGen.
405+
// clone_parallel_type_checker builds a per-worker TypeChecker for parallel codegen.
406+
//
407+
// During codegen the checker's lookup tables are READ-ONLY: cgen only ever assigns the
408+
// scalar `cur_file`/`cur_module` fields, and the read paths it uses (expr_type, c_type,
409+
// parse_type, resolve_type, cached_resolved_call) never write into the big maps — the only
410+
// memoizing write is into `type_cache`, which is left nil here so workers take the uncached
411+
// path. V maps and arrays are reference types, so the read-only tables are SHARED by
412+
// reference (no `.clone()`), exactly like the already-shared `a` FlatAst. This avoids
413+
// deep-copying the program-wide `expr_type_*`/`structs`/signature tables once per worker,
414+
// which was the bulk of parallel cgen's extra RAM and serial setup time.
415+
//
416+
// Only genuinely per-worker mutable state is given its own copy: the scope chain (gen pushes
417+
// child scopes) and `errors` (avoid a concurrent append race, though gen does not emit any).
381418
fn (g &FlatGen) clone_parallel_type_checker() &types.TypeChecker {
382419
mut fs := types.new_scope(unsafe { nil })
383420
fs.names = g.tc.file_scope.names.clone()
384421
fs.types = g.tc.file_scope.types.clone()
385422
return &types.TypeChecker{
386423
a: unsafe { g.tc.a }
387-
fn_ret_types: g.tc.fn_ret_types.clone()
388-
fn_param_types: g.tc.fn_param_types.clone()
389-
fn_variadic: g.tc.fn_variadic.clone()
390-
structs: g.tc.structs.clone()
391-
unions: g.tc.unions.clone()
392-
type_aliases: g.tc.type_aliases.clone()
393-
sum_types: g.tc.sum_types.clone()
394-
enum_names: g.tc.enum_names.clone()
395-
enum_fields: g.tc.enum_fields.clone()
396-
flag_enums: g.tc.flag_enums.clone()
397-
interface_names: g.tc.interface_names.clone()
398-
interface_fields: g.tc.interface_fields.clone()
399-
interface_embeds: g.tc.interface_embeds.clone()
400-
interface_abstract_methods: g.tc.interface_abstract_methods.clone()
401-
c_globals: g.tc.c_globals.clone()
402-
const_types: g.tc.const_types.clone()
403-
const_exprs: g.tc.const_exprs.clone()
404-
const_modules: g.tc.const_modules.clone()
405-
const_suffixes: g.tc.const_suffixes.clone()
406-
imports: g.tc.imports.clone()
407-
file_imports: g.tc.file_imports.clone()
408-
file_modules: g.tc.file_modules.clone()
424+
fn_ret_types: g.tc.fn_ret_types
425+
fn_param_types: g.tc.fn_param_types
426+
fn_variadic: g.tc.fn_variadic
427+
structs: g.tc.structs
428+
unions: g.tc.unions
429+
type_aliases: g.tc.type_aliases
430+
sum_types: g.tc.sum_types
431+
enum_names: g.tc.enum_names
432+
enum_fields: g.tc.enum_fields
433+
flag_enums: g.tc.flag_enums
434+
interface_names: g.tc.interface_names
435+
interface_fields: g.tc.interface_fields
436+
interface_embeds: g.tc.interface_embeds
437+
interface_abstract_methods: g.tc.interface_abstract_methods
438+
c_globals: g.tc.c_globals
439+
const_types: g.tc.const_types
440+
const_exprs: g.tc.const_exprs
441+
const_modules: g.tc.const_modules
442+
const_suffixes: g.tc.const_suffixes
443+
imports: g.tc.imports
444+
file_imports: g.tc.file_imports
445+
file_modules: g.tc.file_modules
409446
file_scope: fs
410447
cur_scope: fs
411448
scope_pool: []&types.Scope{}
412449
has_builtins: g.tc.has_builtins
413450
cur_module: g.tc.cur_module
414451
cur_file: g.tc.cur_file
415452
errors: g.tc.errors.clone()
416-
resolved_call_names: g.tc.resolved_call_names.clone()
417-
resolved_call_set: g.tc.resolved_call_set.clone()
418-
expr_type_values: g.tc.expr_type_values.clone()
419-
expr_type_set: g.tc.expr_type_set.clone()
420-
checking_nodes: g.tc.checking_nodes.clone()
453+
resolved_call_names: g.tc.resolved_call_names
454+
resolved_call_set: g.tc.resolved_call_set
455+
expr_type_values: g.tc.expr_type_values
456+
expr_type_set: g.tc.expr_type_set
457+
checking_nodes: g.tc.checking_nodes
421458
diagnose_unknown_calls: g.tc.diagnose_unknown_calls
422459
reject_unlowered_map_mutation: g.tc.reject_unlowered_map_mutation
423-
diagnostic_files: g.tc.diagnostic_files.clone()
460+
diagnostic_files: g.tc.diagnostic_files
424461
cur_fn_ret_type: g.tc.cur_fn_ret_type
425-
smartcasts: g.tc.smartcasts.clone()
462+
smartcasts: g.tc.smartcasts
426463
}
427464
}
428465

vlib/v3/tests/c_output_only_test.v

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ fn test_c_output_path_only_writes_c_file() {
2121
assert compile.exit_code == 0, compile.output
2222
assert os.exists(c_out)
2323
assert !os.exists(bin_out)
24-
assert compile.output.contains('gen C/write')
24+
assert compile.output.contains('cgen')
2525
assert !compile.output.contains(' > ')
2626
assert !compile.output.contains('tcc.exe')
2727
assert !compile.output.contains('cc -std=gnu11')

vlib/v3/tests/parallel_cgen_test.v

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ fn test_parallel_cgen_main_emits_module_init_call() {
6161
c_out := os.join_path(os.temp_dir(), 'v3_parallel_module_init.c')
6262
compile := os.execute('VJOBS=2 ${v3_bin} ${main_path} -o ${c_out}')
6363
assert compile.exit_code == 0, compile.output
64-
assert compile.output.contains('gen C/write (parallel)'), compile.output
64+
assert compile.output.contains('cgen'), compile.output
6565
c_code := os.read_file(c_out) or { panic(err) }
6666
assert c_code.all_after('int main').contains('_vinit();')
6767
}

vlib/v3/transform/transform_d_parallel.v

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,40 +63,54 @@ fn (mut t Transformer) run_parallel_transform(items []FnWorkItem, base_nodes int
6363
mut chunks := split_work_items(items, n_jobs)
6464
chunk_count := chunks.len
6565

66-
// Build one worker per chunk: a private AST clone + forked TypeChecker.
67-
mut workers := []voidptr{cap: chunk_count}
68-
for _ in 0 .. chunk_count {
66+
// chunk[0] is transformed by the master on this thread, directly against the
67+
// master AST — no clone. Only chunks[1..] get helper threads, each with a
68+
// private AST clone + forked TypeChecker. This removes one full base-AST clone
69+
// from the peak (each clone is ~one nodes-array; under -gc none they are never
70+
// freed, so they also inflate the later cgen peak) and keeps the master thread,
71+
// which would otherwise block in join, doing useful work.
72+
thread_count := chunk_count - 1
73+
mut workers := []voidptr{cap: thread_count}
74+
for _ in 0 .. thread_count {
6975
wast := t.clone_ast_base(base_nodes, base_children)
7076
wtc := t.tc.fork_for_parallel_transform(wast)
7177
ww := t.fork_worker(wast, wtc)
7278
workers << voidptr(ww)
7379
}
74-
mut args := []TransformChunkArgs{cap: chunk_count}
75-
for ci in 0 .. chunk_count {
80+
mut args := []TransformChunkArgs{cap: thread_count}
81+
for ci in 0 .. thread_count {
7682
args << TransformChunkArgs{
7783
worker: workers[ci]
78-
items_ptr: unsafe { voidptr(&chunks[ci]) }
84+
items_ptr: unsafe { voidptr(&chunks[ci + 1]) }
7985
}
8086
}
8187

82-
mut thread_ids := []C.pthread_t{len: chunk_count}
88+
mut thread_ids := []C.pthread_t{len: thread_count}
8389
attr_buf := [64]u8{}
8490
attr := unsafe { voidptr(&attr_buf[0]) }
8591
C.pthread_attr_init(attr)
8692
// Transform recurses deeply on large expressions; give workers a roomy stack.
8793
C.pthread_attr_setstacksize(attr, 64 * 1024 * 1024)
88-
for ci in 0 .. chunk_count {
94+
for ci in 0 .. thread_count {
8995
C.pthread_create(unsafe { &thread_ids[ci] }, attr, transform_chunk_thread,
9096
unsafe { voidptr(&args[ci]) })
9197
}
9298
C.pthread_attr_destroy(attr)
93-
for ci in 0 .. chunk_count {
99+
// Master transforms chunk[0] in place while the helper threads run. It only
100+
// touches its own functions' nodes and the master AST/TypeChecker, all disjoint
101+
// from the workers' clones, and never writes the shared (read-only) type tables.
102+
// Reset temp_counter to 0 like a freshly forked worker (transform temps are
103+
// function-local, so this is collision-free and matches the all-workers output).
104+
t.temp_counter = 0
105+
t.transform_pure_items_serial(chunks[0])
106+
for ci in 0 .. thread_count {
94107
C.pthread_join(thread_ids[ci], unsafe { nil })
95108
}
96-
// Merge in a fixed order for reproducible node numbering.
97-
for ci in 0 .. chunk_count {
109+
// Merge helper results in fixed chunk order (chunk[0] is already in place), so
110+
// node numbering stays deterministic for reproducible builds.
111+
for ci in 0 .. thread_count {
98112
ww := unsafe { &Transformer(workers[ci]) }
99-
t.merge_worker(ww, chunks[ci], base_nodes, base_children)
113+
t.merge_worker(ww, chunks[ci + 1], base_nodes, base_children)
100114
}
101115
return true
102116
}

vlib/v3/v3.v

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ fn main() {
332332
eprintln('error writing ${output_file}')
333333
exit(1)
334334
}
335-
b.step_parallel('gen C/write', g.was_parallel())
335+
b.step_parallel('cgen', g.was_parallel())
336336
if c_only {
337337
b.print_report()
338338
return

0 commit comments

Comments
 (0)