From 1a7f78b66578492103607628d2cd4a2de70b29ec Mon Sep 17 00:00:00 2001 From: Jesse Meyer Date: Sun, 1 Feb 2026 09:04:53 -0500 Subject: [PATCH] Add ThinLTO support via -lto:thin and -lto:thin-files flags - Add -lto:thin and -lto:thin-files CLI flags with validation - Emit LLVM bitcode (.bc) instead of object files when LTO is enabled - Pass -flto=thin and -flto-jobs to clang/lld linkers - Guard linkage corrections to skip declarations without definitions (required for LTO where declarations appear across modules) - Allow module-per-file with LTO even at higher optimization levels Co-Authored-By: Claude Opus 4.5 --- src/build_settings.cpp | 30 ++++++++++++++++++++++++++++++ src/linker.cpp | 16 +++++++++++++++- src/llvm_backend.cpp | 23 ++++++++++++++++++----- src/llvm_backend_general.cpp | 2 +- src/main.cpp | 14 ++++++++++++++ 5 files changed, 78 insertions(+), 7 deletions(-) diff --git a/src/build_settings.cpp b/src/build_settings.cpp index 6b8fefa59..e0017baea 100644 --- a/src/build_settings.cpp +++ b/src/build_settings.cpp @@ -420,6 +420,12 @@ struct BuildCacheData { }; +enum LTOKind : i32 { + LTO_None, + LTO_Thin, + LTO_Thin_Files, +}; + enum LinkerChoice : i32 { Linker_Invalid = -1, Linker_Default = 0, @@ -560,6 +566,7 @@ struct BuildContext { bool use_single_module; bool use_separate_modules; + LTOKind lto_kind; bool module_per_file; bool cached; BuildCacheData build_cache_data; @@ -2030,6 +2037,29 @@ gb_internal void init_build_context(TargetMetrics *cross_target, Subtarget subta bc->use_separate_modules = false; } + if (bc->lto_kind == LTO_Thin || bc->lto_kind == LTO_Thin_Files) { +#if LLVM_VERSION_MAJOR < 17 + gb_printf_err("-lto:thin requires LLVM 17 or later\n"); + gb_exit(1); +#endif + if (bc->build_mode == BuildMode_Assembly || bc->build_mode == BuildMode_LLVM_IR) { + gb_printf_err("-lto:thin is incompatible with -build-mode:asm and -build-mode:llvm-ir\n"); + gb_exit(1); + } +#if defined(GB_SYSTEM_WINDOWS) + if (bc->linker_choice != Linker_lld) { + gb_printf_err("-lto:thin on Windows requires -linker:lld\n"); + gb_exit(1); + } +#endif + if (bc->use_single_module) { + gb_printf_err("Warning: -lto:thin overrides -use-single-module; separate modules will be used\n"); + } + bc->use_separate_modules = true; + if (bc->lto_kind == LTO_Thin_Files) { + bc->module_per_file = true; + } + } bc->ODIN_VALGRIND_SUPPORT = false; if (build_context.metrics.os != TargetOs_windows) { diff --git a/src/linker.cpp b/src/linker.cpp index bc6268049..f369a5a38 100644 --- a/src/linker.cpp +++ b/src/linker.cpp @@ -328,6 +328,12 @@ try_cross_linking:; String windows_sdk_bin_path = path_to_string(heap_allocator(), build_context.build_paths[BuildPath_Win_SDK_Bin_Path]); defer (gb_free(heap_allocator(), windows_sdk_bin_path.text)); + gbString lld_lto_flags = gb_string_make(heap_allocator(), ""); + defer (gb_string_free(lld_lto_flags)); + if (build_context.lto_kind != LTO_None) { + lld_lto_flags = gb_string_append_fmt(lld_lto_flags, "/lldltojobs:%d ", build_context.thread_count); + } + switch (build_context.linker_choice) { case Linker_lld: result = system_exec_command_line_app("msvc-lld-link", @@ -336,13 +342,15 @@ try_cross_linking:; "%.*s " "%.*s " "%s " + "%s " "", LIT(build_context.ODIN_ROOT), object_files, LIT(output_filename), link_settings, LIT(windows_subsystem_names[build_context.ODIN_WINDOWS_SUBSYSTEM]), LIT(build_context.link_flags), LIT(build_context.extra_linker_flags), - lib_str + lib_str, + lld_lto_flags ); if (result) { @@ -966,6 +974,12 @@ try_cross_linking:; link_command_line = gb_string_appendc(link_command_line, clang_path); } link_command_line = gb_string_appendc(link_command_line, " -Wno-unused-command-line-argument "); + + if (build_context.lto_kind != LTO_None) { + link_command_line = gb_string_appendc(link_command_line, " -flto=thin"); + link_command_line = gb_string_append_fmt(link_command_line, " -flto-jobs=%d ", build_context.thread_count); + } + link_command_line = gb_string_appendc(link_command_line, object_files); link_command_line = gb_string_append_fmt(link_command_line, " -o \"%.*s\" ", LIT(output_filename)); link_command_line = gb_string_append_fmt(link_command_line, " %s ", platform_lib_str); diff --git a/src/llvm_backend.cpp b/src/llvm_backend.cpp index c15f326f8..43f6f8f03 100644 --- a/src/llvm_backend.cpp +++ b/src/llvm_backend.cpp @@ -171,7 +171,7 @@ gb_internal void lb_correct_entity_linkage(lbGenerator *gen) { LLVMValueRef other_global = nullptr; if (ec.e->kind == Entity_Variable) { other_global = LLVMGetNamedGlobal(ec.other_module->mod, ec.cname); - if (other_global) { + if (other_global && (LLVMGetInitializer(other_global) != nullptr || LLVMIsExternallyInitialized(other_global))) { LLVM_SET_INTERNAL_WEAK_LINKAGE(other_global); if (!ec.e->Variable.is_export && !ec.e->Variable.is_foreign) { LLVMSetVisibility(other_global, LLVMHiddenVisibility); @@ -179,7 +179,7 @@ gb_internal void lb_correct_entity_linkage(lbGenerator *gen) { } } else if (ec.e->kind == Entity_Procedure) { other_global = LLVMGetNamedFunction(ec.other_module->mod, ec.cname); - if (other_global) { + if (other_global && LLVMCountBasicBlocks(other_global) != 0) { LLVM_SET_INTERNAL_WEAK_LINKAGE(other_global); if (!ec.e->Procedure.is_export && !ec.e->Procedure.is_foreign) { LLVMSetVisibility(other_global, LLVMHiddenVisibility); @@ -2332,7 +2332,12 @@ gb_internal WORKER_TASK_PROC(lb_llvm_emit_worker_proc) { auto wd = cast(lbLLVMEmitWorker *)data; - if (LLVMTargetMachineEmitToFile(wd->target_machine, wd->m->mod, cast(char *)wd->filepath_obj.text, wd->code_gen_file_type, &llvm_error)) { + if (build_context.lto_kind != LTO_None) { + if (LLVMWriteBitcodeToFile(wd->m->mod, cast(char *)wd->filepath_obj.text)) { + gb_printf_err("Failed to write bitcode file: %.*s\n", LIT(wd->filepath_obj)); + exit_with_errors(); + } + } else if (LLVMTargetMachineEmitToFile(wd->target_machine, wd->m->mod, cast(char *)wd->filepath_obj.text, wd->code_gen_file_type, &llvm_error)) { gb_printf_err("LLVM Error: %s\n", llvm_error); exit_with_errors(); } @@ -2701,7 +2706,9 @@ gb_internal String lb_filepath_obj_for_module(lbModule *m) { String ext = {}; - if (build_context.build_mode == BuildMode_Assembly) { + if (build_context.lto_kind != LTO_None) { + ext = STR_LIT("bc"); + } else if (build_context.build_mode == BuildMode_Assembly) { ext = STR_LIT("S"); } else if (build_context.build_mode == BuildMode_Object) { // Allow a user override for the object extension. @@ -2776,7 +2783,13 @@ gb_internal bool lb_llvm_object_generation(lbGenerator *gen, bool do_threading) TIME_SECTION_WITH_LEN(section_name, gb_string_length(section_name)); - if (LLVMTargetMachineEmitToFile(m->target_machine, m->mod, cast(char *)filepath_obj.text, code_gen_file_type, &llvm_error)) { + if (build_context.lto_kind != LTO_None) { + if (LLVMWriteBitcodeToFile(m->mod, cast(char *)filepath_obj.text)) { + gb_printf_err("Failed to write bitcode file: %.*s\n", LIT(filepath_obj)); + exit_with_errors(); + return false; + } + } else if (LLVMTargetMachineEmitToFile(m->target_machine, m->mod, cast(char *)filepath_obj.text, code_gen_file_type, &llvm_error)) { gb_printf_err("LLVM Error: %s\n", llvm_error); exit_with_errors(); return false; diff --git a/src/llvm_backend_general.cpp b/src/llvm_backend_general.cpp index c0e57143d..572e3990c 100644 --- a/src/llvm_backend_general.cpp +++ b/src/llvm_backend_general.cpp @@ -163,7 +163,7 @@ gb_internal bool lb_init_generator(lbGenerator *gen, Checker *c) { map_init(&gen->modules_through_ctx, gen->info->packages.count*2); if (USE_SEPARATE_MODULES) { - bool module_per_file = build_context.module_per_file && build_context.optimization_level <= 0; + bool module_per_file = build_context.module_per_file && (build_context.optimization_level <= 0 || build_context.lto_kind != LTO_None); for (auto const &entry : gen->info->packages) { AstPackage *pkg = entry.value; auto m = gb_alloc_item(permanent_allocator(), lbModule); diff --git a/src/main.cpp b/src/main.cpp index 624869e65..feaf55f4b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -413,6 +413,7 @@ enum BuildFlagKind { BuildFlag_Tilde, BuildFlag_Sanitize, + BuildFlag_LTO, #if defined(GB_SYSTEM_WINDOWS) BuildFlag_IgnoreVsSearch, @@ -643,6 +644,7 @@ gb_internal bool parse_build_flags(Array args) { #endif add_flag(&build_flags, BuildFlag_Sanitize, str_lit("sanitize"), BuildFlagParam_String, Command__does_build, true); + add_flag(&build_flags, BuildFlag_LTO, str_lit("lto"), BuildFlagParam_String, Command__does_build); #if defined(GB_SYSTEM_WINDOWS) @@ -1636,6 +1638,18 @@ gb_internal bool parse_build_flags(Array args) { } break; + case BuildFlag_LTO: + GB_ASSERT(value.kind == ExactValue_String); + if (str_eq_ignore_case(value.value_string, str_lit("thin"))) { + build_context.lto_kind = LTO_Thin; + } else if (str_eq_ignore_case(value.value_string, str_lit("thin-files"))) { + build_context.lto_kind = LTO_Thin_Files; + } else { + gb_printf_err("-lto: options are 'thin' and 'thin-files'\n"); + bad_flags = true; + } + break; + #if defined(GB_SYSTEM_WINDOWS) case BuildFlag_IgnoreVsSearch: {