diff --git a/lld/ELF/Config.h b/lld/ELF/Config.h --- a/lld/ELF/Config.h +++ b/lld/ELF/Config.h @@ -192,6 +192,7 @@ bool ltoPGOWarnMismatch; bool ltoDebugPassManager; bool ltoEmitAsm; + llvm::StringRef ltoExitOn; bool ltoUniqueBasicBlockSectionNames; bool ltoWholeProgramVisibility; bool mergeArmExidx; diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp --- a/lld/ELF/Driver.cpp +++ b/lld/ELF/Driver.cpp @@ -504,6 +504,10 @@ "resolution", "preopt", "promote", "internalize", "import", "opt", "precodegen", "prelink", "combinedindex"}; +constexpr const char *ltoExitOnValues[] = {"preopt", "promote", "internalize", + "import", "opt", "precodegen", + "prelink"}; + void LinkerDriver::linkerMain(ArrayRef argsArr) { ELFOptTable parser; opt::InputArgList args = parser.parse(argsArr.slice(1)); @@ -1099,6 +1103,12 @@ OPT_no_lto_pgo_warn_mismatch, true); config->ltoDebugPassManager = args.hasArg(OPT_lto_debug_pass_manager); config->ltoEmitAsm = args.hasArg(OPT_lto_emit_asm); + config->ltoExitOn = args.getLastArgValue(OPT_lto_exit_on_eq); + if (!config->ltoExitOn.empty() && + !llvm::is_contained(ltoExitOnValues, config->ltoExitOn)) { + error("unknown --lto-exit-on value: " + config->ltoExitOn); + config->ltoExitOn = ""; + } config->ltoNewPmPasses = args.getLastArgValue(OPT_lto_newpm_passes); config->ltoWholeProgramVisibility = args.hasFlag(OPT_lto_whole_program_visibility, @@ -2595,9 +2605,11 @@ // --plugin-opt=emit-asm create output files in bitcode or assembly code, // respectively. When only certain thinLTO modules are specified for // compilation, the intermediate object file are the expected output. - const bool skipLinkedOutput = config->thinLTOIndexOnly || config->emitLLVM || - config->ltoEmitAsm || - !config->thinLTOModulesToCompile.empty(); + // --lto-exit-on is specified to prevent the linker from executing after + // certain LTO stages. + const bool skipLinkedOutput = + config->thinLTOIndexOnly || config->emitLLVM || config->ltoEmitAsm || + !config->ltoExitOn.empty() || !config->thinLTOModulesToCompile.empty(); // Do link-time optimization if given files are LLVM bitcode files. // This compiles bitcode files into real object files. diff --git a/lld/ELF/LTO.cpp b/lld/ELF/LTO.cpp --- a/lld/ELF/LTO.cpp +++ b/lld/ELF/LTO.cpp @@ -182,6 +182,9 @@ checkError(c.addSaveTemps(config->outputFile.str() + ".", /*UseInputModulePath*/ true, config->saveTempsArgs)); + + if (!config->ltoExitOn.empty() && config->ltoExitOn != "prelink") + checkError(c.addExitOn(config->ltoExitOn)); return c; } diff --git a/lld/ELF/Options.td b/lld/ELF/Options.td --- a/lld/ELF/Options.td +++ b/lld/ELF/Options.td @@ -549,6 +549,9 @@ HelpText<"Debug new pass manager">; def lto_emit_asm: FF<"lto-emit-asm">, HelpText<"Emit assembly code">; +def lto_exit_on_eq: JJ<"lto-exit-on=">, + HelpText<"Stop the linker at a given stage. If using ThinLTO, linker will only exit after the given ThinLTO stage.">, + Values<"preopt,promote,internalize,import,opt,precodegen,prelink">; def no_lto_legacy_pass_manager: FF<"no-lto-legacy-pass-manager">, HelpText<"Use the new pass manager in LLVM">; def lto_newpm_passes: JJ<"lto-newpm-passes=">, diff --git a/lld/test/ELF/lto/lto-exit-on.ll b/lld/test/ELF/lto/lto-exit-on.ll new file mode 100644 --- /dev/null +++ b/lld/test/ELF/lto/lto-exit-on.ll @@ -0,0 +1,125 @@ +;; This test is similar to llvm/test/ThinLTO/X86/exit-on.ll + +; REQUIRES: x86 +; UNSUPPORTED: system-windows +;; Unsupported on Windows due to difficulty with escaping "opt" across platforms. +;; lit substitutes 'opt' with /path/to/opt. + +; RUN: rm -fr %t && mkdir %t && cd %t +; RUN: mkdir all all2 build +; RUN: cd build + +; RUN: opt -thinlto-bc -o main.o %s +; RUN: opt -thinlto-bc -o thin1.o %S/Inputs/thinlto.ll + +;; Check preopt +; RUN: rm -f *.o.* +; RUN: ld.lld main.o thin1.o --save-temps --lto-exit-on=preopt +; RUN: ls *.0.preopt* +; RUN: not ls *.1.promote* +; RUN: not ls *.2.internalize* +; RUN: not ls *.3.import* +; RUN: not ls *.4.opt* +; RUN: not ls *.5.precodegen* +; RUN: ls a.out*.lto.o +; RUN: not ls a.out + +;; Check promote +; RUN: rm -f *.o.* +; RUN: ld.lld main.o thin1.o --save-temps --lto-exit-on=promote +; RUN: ls *.0.preopt* +; RUN: ls *.1.promote* +;; 1 file is expected due to full LTO outputting .internalize bc +;; before the .promote hook (unique to ThinLTO) is hit +; RUN: ls *.2.internalize* | count 1 +; RUN: not ls *.3.import* +; RUN: not ls *.4.opt* +; RUN: not ls *.5.precodegen* +; RUN: ls a.out*.lto.o +; RUN: not ls a.out + +;; Check internalize +; RUN: rm -f *.o.* +; RUN: ld.lld main.o thin1.o --save-temps --lto-exit-on=internalize +; RUN: ls *.0.preopt* +; RUN: ls *.1.promote* +;; 3 files are expected here and beyond due to full LTO outputting .internalize bc +;; and the ThinLTO internalize hook outputting 2 more files +; RUN: ls *.2.internalize* | count 3 +; RUN: not ls *.3.import* +; RUN: not ls *.4.opt* +; RUN: not ls *.5.precodegen* +; RUN: ls a.out*.lto.o +; RUN: not ls a.out + +;; Check import +; RUN: rm -f *.o.* +; RUN: ld.lld main.o thin1.o --save-temps --lto-exit-on=import +; RUN: ls *.0.preopt* +; RUN: ls *.1.promote* +; RUN: ls *.2.internalize* | count 3 +; RUN: ls *.3.import* +; RUN: not ls *.4.opt* +; RUN: not ls *.5.precodegen* +; RUN: ls a.out*.lto.o +; RUN: not ls a.out + +;; Check opt +; RUN: rm -f *.o.* +; RUN: ld.lld main.o thin1.o --save-temps --lto-exit-on=\opt +; RUN: ls *.0.preopt* +; RUN: ls *.1.promote* +; RUN: ls *.2.internalize* | count 3 +; RUN: ls *.3.import* +; RUN: ls *.4.opt* +; RUN: not ls *.5.precodegen* +; RUN: ls a.out*.lto.o +; RUN: not ls a.out + +;; Check precodegen +; RUN: rm -f *.o.* +; RUN: ld.lld main.o thin1.o --save-temps --lto-exit-on=precodegen +; RUN: ls *.0.preopt* +; RUN: ls *.1.promote* +; RUN: ls *.2.internalize* | count 3 +; RUN: ls *.3.import* +; RUN: ls *.4.opt* +; RUN: ls *.5.precodegen* +; RUN: ls a.out*.lto.o +; RUN: not ls a.out + +;; Check prelink +; RUN: rm -f *.o.* a.out* +; RUN: ld.lld main.o thin1.o --save-temps --lto-exit-on=prelink +; RUN: ls *.0.preopt* +; RUN: ls *.1.promote* +; RUN: ls *.2.internalize* | count 3 +; RUN: ls *.3.import* +; RUN: ls *.4.opt* +; RUN: ls *.5.precodegen* +; RUN: ls a.out*.lto.o +; RUN: not ls a.out + +;; Check output files are as expected +; RUN: mv *.o.* a.out* %t/all2 + +;; Create the .all dir with save-temps, without exit-on, then diff +; RUN: ld.lld main.o thin1.o --save-temps +; RUN: rm a.out +; RUN: mv *.o.* a.out* %t/all +; RUN: diff -r %t/all %t/all2 + +;; Check input validation +; RUN: not ld.lld main.o thin1.o --save-temps --lto-exit-on=notastage 2>&1 \ +; RUN: | FileCheck %s --check-prefix=ERR1 +; ERR1: unknown --lto-exit-on value: notastage + +target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" +target triple = "x86_64-unknown-linux-gnu" + +declare void @g() + +define i32 @_start() { + call void @g() + ret i32 0 +} diff --git a/llvm/include/llvm/LTO/Config.h b/llvm/include/llvm/LTO/Config.h --- a/llvm/include/llvm/LTO/Config.h +++ b/llvm/include/llvm/LTO/Config.h @@ -273,6 +273,13 @@ Error addSaveTemps(std::string OutputFileName, bool UseInputModulePath = false, const DenseSet &SaveTempsArgs = {}); + + /// Configures this Config object to stop executing further LTO/linker stages + /// upon reaching a specified stage. + /// This is done by modifying the above module hooks such that they: + /// 1) Run the existing hook, if any. + /// 2) return false. + Error addExitOn(StringRef LTOExitOn); }; struct LTOLLVMDiagnosticHandler : public DiagnosticHandler { diff --git a/llvm/lib/LTO/LTOBackend.cpp b/llvm/lib/LTO/LTOBackend.cpp --- a/llvm/lib/LTO/LTOBackend.cpp +++ b/llvm/lib/LTO/LTOBackend.cpp @@ -31,6 +31,7 @@ #include "llvm/Passes/PassBuilder.h" #include "llvm/Passes/PassPlugin.h" #include "llvm/Passes/StandardInstrumentations.h" +#include "llvm/Support/Errc.h" #include "llvm/Support/Error.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/MemoryBuffer.h" @@ -175,6 +176,36 @@ return Error::success(); } +Error Config::addExitOn(StringRef LTOExitOn) { + auto setHook = [&](ModuleHookFn &Hook) { + // Keep track of the hook provided by the linker, which needs to run first. + ModuleHookFn LinkerHook = Hook; + Hook = [=](unsigned Task, const Module &M) { + if (LinkerHook) + LinkerHook(Task, M); + return false; + }; + }; + + if (LTOExitOn == "preopt") + setHook(PreOptModuleHook); + else if (LTOExitOn == "promote") + setHook(PostPromoteModuleHook); + else if (LTOExitOn == "internalize") + setHook(PostInternalizeModuleHook); + else if (LTOExitOn == "import") + setHook(PostImportModuleHook); + else if (LTOExitOn == "opt") + setHook(PostOptModuleHook); + else if (LTOExitOn == "precodegen") + setHook(PreCodeGenModuleHook); + else + return make_error("invalid addExitOn parameter: " + LTOExitOn, + errc::invalid_argument); + + return Error::success(); +} + #define HANDLE_EXTENSION(Ext) \ llvm::PassPluginLibraryInfo get##Ext##PluginInfo(); #include "llvm/Support/Extension.def" diff --git a/llvm/test/ThinLTO/X86/exit-on.ll b/llvm/test/ThinLTO/X86/exit-on.ll new file mode 100644 --- /dev/null +++ b/llvm/test/ThinLTO/X86/exit-on.ll @@ -0,0 +1,136 @@ +; UNSUPPORTED: system-windows +;; Unsupported on Windows due to difficulty with escaping "opt" across platforms. +;; lit substitutes 'opt' with /path/to/opt. + +; RUN: rm -rf %t && mkdir %t && cd %t + +;; Copy IR from import-constant.ll since it generates all the temps +; RUN: opt -thinlto-bc %s -o 1.bc +; RUN: opt -thinlto-bc %p/Inputs/import-constant.ll -o 2.bc +; RUN: mkdir all build + +;; Check preopt +; RUN: llvm-lto2 run 1.bc 2.bc -o build/a.out \ +; RUN: -import-constants-with-refs -r=1.bc,main,plx -r=1.bc,_Z6getObjv,l \ +; RUN: -r=2.bc,_Z6getObjv,pl -r=2.bc,val,pl -r=2.bc,outer,pl \ +; RUN: -save-temps -exit-on=preopt +; RUN: ls build/*.0.preopt* +; RUN: not ls build/*.1.promote* +; RUN: not ls build/*.2.internalize* +; RUN: not ls build/*.3.import* +; RUN: not ls build/*.4.opt* +; RUN: not ls build/*.5.precodegen* +; RUN: not ls build/a.out.1 +; RUN: not ls build/a.out.2 + +;; Check promote +; RUN: rm -f build/* +; RUN: llvm-lto2 run 1.bc 2.bc -o build/a.out \ +; RUN: -import-constants-with-refs -r=1.bc,main,plx -r=1.bc,_Z6getObjv,l \ +; RUN: -r=2.bc,_Z6getObjv,pl -r=2.bc,val,pl -r=2.bc,outer,pl \ +; RUN: -save-temps -exit-on=promote +; RUN: ls build/*.0.preopt* +; RUN: ls build/*.1.promote* +;; 1 file is expected due to full LTO outputting .internalize bc +;; before the .promote hook (unique to ThinLTO) is hit +; RUN: ls build/*.2.internalize* | count 1 +; RUN: not ls build/*.3.import* +; RUN: not ls build/*.4.opt* +; RUN: not ls build/*.5.precodegen* +; RUN: not ls build/a.out.1 +; RUN: not ls build/a.out.2 + +;; Check internalize +; RUN: rm -f build/* +; RUN: llvm-lto2 run 1.bc 2.bc -o build/a.out \ +; RUN: -import-constants-with-refs -r=1.bc,main,plx -r=1.bc,_Z6getObjv,l \ +; RUN: -r=2.bc,_Z6getObjv,pl -r=2.bc,val,pl -r=2.bc,outer,pl \ +; RUN: -save-temps -exit-on=internalize +; RUN: ls build/*.0.preopt* +; RUN: ls build/*.1.promote* +;; 3 files are expected here and beyond due to full LTO outputting .internalize bc +;; and the ThinLTO internalize hook outputting 2 more files +; RUN: ls build/*.2.internalize* | count 3 +; RUN: not ls build/*.3.import* +; RUN: not ls build/*.4.opt* +; RUN: not ls build/*.5.precodegen* +; RUN: not ls build/a.out.1 +; RUN: not ls build/a.out.2 + +;; Check import +; RUN: rm -f build/* +; RUN: llvm-lto2 run 1.bc 2.bc -o build/a.out \ +; RUN: -import-constants-with-refs -r=1.bc,main,plx -r=1.bc,_Z6getObjv,l \ +; RUN: -r=2.bc,_Z6getObjv,pl -r=2.bc,val,pl -r=2.bc,outer,pl \ +; RUN: -save-temps -exit-on=import +; RUN: ls build/*.0.preopt* +; RUN: ls build/*.1.promote* +; RUN: ls build/*.2.internalize* | count 3 +; RUN: ls build/*.3.import* +; RUN: not ls build/*.4.opt* +; RUN: not ls build/*.5.precodegen* +; RUN: not ls build/a.out.1 +; RUN: not ls build/a.out.2 + +;; Check opt +; RUN: rm -f build/* +; RUN: llvm-lto2 run 1.bc 2.bc -o build/a.out \ +; RUN: -import-constants-with-refs -r=1.bc,main,plx -r=1.bc,_Z6getObjv,l \ +; RUN: -r=2.bc,_Z6getObjv,pl -r=2.bc,val,pl -r=2.bc,outer,pl \ +; RUN: -save-temps -exit-on=\opt +; RUN: ls build/*.0.preopt* +; RUN: ls build/*.1.promote* +; RUN: ls build/*.2.internalize* | count 3 +; RUN: ls build/*.3.import* +; RUN: ls build/*.4.opt* +; RUN: not ls build/*.5.precodegen* +; RUN: not ls build/a.out.1 +; RUN: not ls build/a.out.2 + +;; Check precodegen +; RUN: llvm-lto2 run 1.bc 2.bc -o build/a.out \ +; RUN: -import-constants-with-refs -r=1.bc,main,plx -r=1.bc,_Z6getObjv,l \ +; RUN: -r=2.bc,_Z6getObjv,pl -r=2.bc,val,pl -r=2.bc,outer,pl \ +; RUN: -save-temps -exit-on=precodegen +; RUN: ls build/*.0.preopt* +; RUN: ls build/*.1.promote* +; RUN: ls build/*.2.internalize* | count 3 +; RUN: ls build/*.3.import* +; RUN: ls build/*.4.opt* +; RUN: ls build/*.5.precodegen* +; RUN: not ls build/a.out.1 +; RUN: not ls build/a.out.2 + +;; Create the .all dir with save-temps, without exit-on, then diff +; RUN: llvm-lto2 run 1.bc 2.bc -o all/a.out \ +; RUN: -import-constants-with-refs -r=1.bc,main,plx -r=1.bc,_Z6getObjv,l \ +; RUN: -r=2.bc,_Z6getObjv,pl -r=2.bc,val,pl -r=2.bc,outer,pl \ +; RUN: -save-temps +; RUN: rm all/a.out.1 all/a.out.2 +; RUN: diff -r all build + +;; Check error message +; RUN: not llvm-lto2 run 1.bc 2.bc -o all/a.out \ +; RUN: -import-constants-with-refs -r=1.bc,main,plx -r=1.bc,_Z6getObjv,l \ +; RUN: -r=2.bc,_Z6getObjv,pl -r=2.bc,val,pl -r=2.bc,outer,pl \ +; RUN: -exit-on=prelink 2>&1 \ +; RUN: | FileCheck %s --check-prefix=ERR1 +; ERR1: invalid addExitOn parameter: prelink + +target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" +target triple = "x86_64-unknown-linux-gnu" + +%struct.S = type { i32, i32, i32* } + +define dso_local i32 @main() local_unnamed_addr { +entry: + %call = tail call %struct.S* @_Z6getObjv() + %d = getelementptr inbounds %struct.S, %struct.S* %call, i64 0, i32 0 + %0 = load i32, i32* %d, align 8 + %v = getelementptr inbounds %struct.S, %struct.S* %call, i64 0, i32 1 + %1 = load i32, i32* %v, align 4 + %add = add nsw i32 %1, %0 + ret i32 %add +} + +declare dso_local %struct.S* @_Z6getObjv() local_unnamed_addr diff --git a/llvm/tools/llvm-lto2/llvm-lto2.cpp b/llvm/tools/llvm-lto2/llvm-lto2.cpp --- a/llvm/tools/llvm-lto2/llvm-lto2.cpp +++ b/llvm/tools/llvm-lto2/llvm-lto2.cpp @@ -65,6 +65,10 @@ cl::desc("Alias Analysis Pipeline"), cl::value_desc("aapipeline")); +static cl::opt ExitOn( + "exit-on", cl::desc("Stop LTO at a given stage"), + cl::value_desc("One of: preopt,promote,internalize,import,opt,precodegen")); + static cl::opt SaveTemps("save-temps", cl::desc("Save temporary files")); static cl::list SelectSaveTemps( @@ -288,6 +292,9 @@ "Config::addSaveTemps failed"); } + if (!ExitOn.empty()) + check(Conf.addExitOn(ExitOn), "Config::addExitOn failed"); + // Optimization remarks. Conf.RemarksFilename = RemarksFilename; Conf.RemarksPasses = RemarksPasses;