From a75cd076edd6fa3d69a4cced153c04bc4e9d3ba7 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Thu, 26 Feb 2026 11:24:35 +0300 Subject: [PATCH] build: use PGO in -prod (fixes #7024) --- cmd/tools/vself.v | 196 +++++++++++++++++++++++++++++- vlib/v/help/installation/self.txt | 1 + 2 files changed, 196 insertions(+), 1 deletion(-) diff --git a/cmd/tools/vself.v b/cmd/tools/vself.v index 78ee11f54..a95c6fc74 100644 --- a/cmd/tools/vself.v +++ b/cmd/tools/vself.v @@ -46,7 +46,18 @@ fn main() { cmd := '${os.quoted_path(vexe)} ${sargs} ${os.quoted_path('cmd/v')}' options := if args.len > 0 { '(${sargs})' } else { '' } println('V self compiling ${options}...') - compile(vroot, cmd) + final_binary := if obinary != '' { obinary } else { 'v2' } + mut used_pgo := false + pgo_cc_kind := pgo_compiler_kind(args) + if pgo_cc_kind != '' { + used_pgo = compile_with_pgo(vroot, vexe, args, final_binary, pgo_cc_kind) + if !used_pgo { + eprintln('PGO self-build failed; falling back to a regular self-build.') + } + } + if !used_pgo { + compile(vroot, cmd) + } if obinary != '' { // When -o was given, there is no need to backup/rename the original. // The user just wants an independent copy of v, and so we are done. @@ -68,6 +79,189 @@ fn has_gc_arg(args []string) bool { return false } +fn has_profile_cflag(args []string) bool { + mut skip_next := false + for i, arg in args { + if skip_next { + skip_next = false + continue + } + if arg in ['-cflags', '-cf'] { + if i + 1 < args.len { + next_arg := args[i + 1] + if next_arg.contains('-fprofile') { + return true + } + skip_next = true + } + continue + } + if (arg.starts_with('-cflags=') || arg.starts_with('-cf=')) && arg.contains('-fprofile') { + return true + } + } + return false +} + +fn pgo_compiler_kind(args []string) string { + if '-prod' !in args || '-no-prod-options' in args { + return '' + } + if os.user_os() == 'windows' { + return '' + } + if has_profile_cflag(args) { + return '' + } + mut ccompiler := cmdline.option(args, '-cc', '') + if ccompiler == '' { + ccompiler = os.getenv_opt('CC') or { 'cc' } + } + cc_file_name := os.file_name(ccompiler) + if cc_file_name.contains('clang') || cc_file_name.contains('gcc') + || cc_file_name.contains('g++') || ccompiler == 'cc' { + cc_ver := os.execute('${os.quoted_path(ccompiler)} --version').output + if cc_ver.contains('clang') { + _ := find_llvm_profdata() or { return '' } + return 'clang' + } + if cc_ver.contains('Free Software Foundation') || cc_ver.contains('GCC') { + return 'gcc' + } + } + if cc_file_name.contains('clang') { + _ := find_llvm_profdata() or { return '' } + return 'clang' + } + if cc_file_name.contains('gcc') || cc_file_name.contains('g++') { + return 'gcc' + } + return '' +} + +fn find_llvm_profdata() !string { + if profdata := os.find_abs_path_of_executable('llvm-profdata') { + return profdata + } + $if macos { + xcrun_result := os.execute('xcrun --find llvm-profdata') + if xcrun_result.exit_code == 0 { + xcrun_path := xcrun_result.output.trim_space() + if xcrun_path != '' && os.exists(xcrun_path) { + return xcrun_path + } + } + } + return error('can not find llvm-profdata in PATH') +} + +fn with_output_arg(args []string, output string) []string { + mut res := []string{cap: args.len + 2} + mut skip_next := false + for i, arg in args { + if skip_next { + skip_next = false + continue + } + if arg == '-o' { + if i + 1 < args.len { + skip_next = true + } + continue + } + if arg.starts_with('-o=') { + continue + } + res << arg + } + res << ['-o', output] + return res +} + +fn compose_v_cmd(vexe string, args []string, source string) string { + mut parts := []string{cap: args.len + 2} + parts << os.quoted_path(vexe) + for arg in args { + parts << os.quoted_path(arg) + } + parts << os.quoted_path(source) + return parts.join(' ') +} + +fn run_cmd(cmd string) ! { + result := os.execute(cmd) + if result.exit_code != 0 { + return error(result.output) + } + if result.output.len > 0 { + println(result.output.trim_space()) + } +} + +fn compile_with_pgo(vroot string, vexe string, args []string, out_binary string, cc_kind string) bool { + pgo_workspace := os.join_path(vroot, '.vself_pgo') + os.rmdir_all(pgo_workspace) or {} + os.mkdir_all(pgo_workspace) or { + eprintln('PGO disabled: can not create ${pgo_workspace}: ${err.msg()}') + return false + } + defer { + os.rmdir_all(pgo_workspace) or {} + } + profile_dir := os.join_path(pgo_workspace, 'profile') + os.mkdir_all(profile_dir) or { + eprintln('PGO disabled: can not create ${profile_dir}: ${err.msg()}') + return false + } + pgo_binary := os.join_path(pgo_workspace, 'v_pgo_gen') + training_output := os.join_path(pgo_workspace, 'cmd_v_training.c') + mut use_profile_flag := '-fprofile-use=${profile_dir}' + mut llvm_profdata := '' + mut profile_data := '' + if cc_kind == 'clang' { + llvm_profdata = find_llvm_profdata() or { + eprintln('PGO disabled: can not find `llvm-profdata`.') + return false + } + profile_data = os.join_path(pgo_workspace, 'code.profdata') + use_profile_flag = '-fprofile-use=${profile_data}' + } + mut generate_args := with_output_arg(args, pgo_binary) + generate_args << ['-cflags', '-fprofile-generate=${profile_dir}'] + generate_cmd := compose_v_cmd(vexe, generate_args, 'cmd/v') + run_cmd(generate_cmd) or { + eprintln('PGO step failed while building the instrumented compiler.') + eprintln(err.msg()) + return false + } + training_cmd := '${os.quoted_path(pgo_binary)} -o ${os.quoted_path(training_output)} ${os.quoted_path('cmd/v')}' + run_cmd(training_cmd) or { + eprintln('PGO step failed while generating the profiling data.') + eprintln(err.msg()) + return false + } + if cc_kind == 'clang' { + merge_cmd := '${os.quoted_path(llvm_profdata)} merge -output=${os.quoted_path(profile_data)} ${os.quoted_path(profile_dir)}' + run_cmd(merge_cmd) or { + eprintln('PGO step failed while merging the profiling data.') + eprintln(err.msg()) + return false + } + } + mut final_args := with_output_arg(args, out_binary) + final_args << ['-cflags', use_profile_flag] + if cc_kind == 'gcc' { + final_args << ['-cflags', '-fprofile-correction'] + } + final_cmd := compose_v_cmd(vexe, final_args, 'cmd/v') + run_cmd(final_cmd) or { + eprintln('PGO step failed while building the final compiler binary.') + eprintln(err.msg()) + return false + } + return true +} + fn compile(vroot string, cmd string) { result := os.execute_or_exit(cmd) if result.exit_code != 0 { diff --git a/vlib/v/help/installation/self.txt b/vlib/v/help/installation/self.txt index 2c18dede9..b20a09395 100644 --- a/vlib/v/help/installation/self.txt +++ b/vlib/v/help/installation/self.txt @@ -5,3 +5,4 @@ Usage: Options: All other options are passed to the build command. (e.g. -prod) + With -prod and clang/gcc, `v self` may use PGO automatically when tools are available. -- 2.39.5