Fix linker-plugin-lto only doing thin lto

When rust provides LLVM bitcode files to lld and the bitcode contains
function summaries as used for thin lto, lld defaults to using thin lto.
This prevents some optimizations that are only applied for fat lto.

We solve this by not creating function summaries when fat lto is
enabled. The bitcode for the module is just directly written out.

An alternative solution would be to set the `ThinLTO=0` module flag to
signal lld to do fat lto.
The code in clang that sets this flag is here:
560149b5e3/clang/lib/CodeGen/BackendUtil.cpp (L1150)
The code in LLVM that queries the flag and defaults to thin lto if not
set is here:
e258bca950/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp (L4441-L4446)
This commit is contained in:
Flakebi
2025-03-28 10:15:56 +01:00
parent eed187cfce
commit 7a127fba65
11 changed files with 189 additions and 25 deletions

View File

@@ -215,7 +215,9 @@ impl ModuleConfig {
false false
), ),
emit_obj, emit_obj,
emit_thin_lto: sess.opts.unstable_opts.emit_thin_lto, // thin lto summaries prevent fat lto, so do not emit them if fat
// lto is requested. See PR #136840 for background information.
emit_thin_lto: sess.opts.unstable_opts.emit_thin_lto && sess.lto() != Lto::Fat,
emit_thin_lto_summary: if_regular!( emit_thin_lto_summary: if_regular!(
sess.opts.output_types.contains_key(&OutputType::ThinLinkBitcode), sess.opts.output_types.contains_key(&OutputType::ThinLinkBitcode),
false false

View File

@@ -60,6 +60,12 @@ pub fn llvm_pdbutil() -> LlvmPdbutil {
LlvmPdbutil::new() LlvmPdbutil::new()
} }
/// Construct a new `llvm-as` invocation. This assumes that `llvm-as` is available
/// at `$LLVM_BIN_DIR/llvm-as`.
pub fn llvm_as() -> LlvmAs {
LlvmAs::new()
}
/// Construct a new `llvm-dis` invocation. This assumes that `llvm-dis` is available /// Construct a new `llvm-dis` invocation. This assumes that `llvm-dis` is available
/// at `$LLVM_BIN_DIR/llvm-dis`. /// at `$LLVM_BIN_DIR/llvm-dis`.
pub fn llvm_dis() -> LlvmDis { pub fn llvm_dis() -> LlvmDis {
@@ -135,6 +141,13 @@ pub struct LlvmPdbutil {
cmd: Command, cmd: Command,
} }
/// A `llvm-as` invocation builder.
#[derive(Debug)]
#[must_use]
pub struct LlvmAs {
cmd: Command,
}
/// A `llvm-dis` invocation builder. /// A `llvm-dis` invocation builder.
#[derive(Debug)] #[derive(Debug)]
#[must_use] #[must_use]
@@ -158,6 +171,7 @@ crate::macros::impl_common_helpers!(LlvmNm);
crate::macros::impl_common_helpers!(LlvmBcanalyzer); crate::macros::impl_common_helpers!(LlvmBcanalyzer);
crate::macros::impl_common_helpers!(LlvmDwarfdump); crate::macros::impl_common_helpers!(LlvmDwarfdump);
crate::macros::impl_common_helpers!(LlvmPdbutil); crate::macros::impl_common_helpers!(LlvmPdbutil);
crate::macros::impl_common_helpers!(LlvmAs);
crate::macros::impl_common_helpers!(LlvmDis); crate::macros::impl_common_helpers!(LlvmDis);
crate::macros::impl_common_helpers!(LlvmObjcopy); crate::macros::impl_common_helpers!(LlvmObjcopy);
@@ -441,6 +455,22 @@ impl LlvmObjcopy {
} }
} }
impl LlvmAs {
/// Construct a new `llvm-as` invocation. This assumes that `llvm-as` is available
/// at `$LLVM_BIN_DIR/llvm-as`.
pub fn new() -> Self {
let llvm_as = llvm_bin_dir().join("llvm-as");
let cmd = Command::new(llvm_as);
Self { cmd }
}
/// Provide an input file.
pub fn input<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
self.cmd.arg(path.as_ref());
self
}
}
impl LlvmDis { impl LlvmDis {
/// Construct a new `llvm-dis` invocation. This assumes that `llvm-dis` is available /// Construct a new `llvm-dis` invocation. This assumes that `llvm-dis` is available
/// at `$LLVM_BIN_DIR/llvm-dis`. /// at `$LLVM_BIN_DIR/llvm-dis`.

View File

@@ -173,6 +173,12 @@ impl Rustc {
self self
} }
/// This flag enables LTO in the specified form.
pub fn lto(&mut self, option: &str) -> &mut Self {
self.cmd.arg(format!("-Clto={option}"));
self
}
/// This flag defers LTO optimizations to the linker. /// This flag defers LTO optimizations to the linker.
pub fn linker_plugin_lto(&mut self, option: &str) -> &mut Self { pub fn linker_plugin_lto(&mut self, option: &str) -> &mut Self {
self.cmd.arg(format!("-Clinker-plugin-lto={option}")); self.cmd.arg(format!("-Clinker-plugin-lto={option}"));

View File

@@ -63,8 +63,9 @@ pub use crate::external_deps::clang::{Clang, clang};
pub use crate::external_deps::htmldocck::htmldocck; pub use crate::external_deps::htmldocck::htmldocck;
pub use crate::external_deps::llvm::{ pub use crate::external_deps::llvm::{
self, LlvmAr, LlvmBcanalyzer, LlvmDis, LlvmDwarfdump, LlvmFilecheck, LlvmNm, LlvmObjcopy, self, LlvmAr, LlvmBcanalyzer, LlvmDis, LlvmDwarfdump, LlvmFilecheck, LlvmNm, LlvmObjcopy,
LlvmObjdump, LlvmProfdata, LlvmReadobj, llvm_ar, llvm_bcanalyzer, llvm_dis, llvm_dwarfdump, LlvmObjdump, LlvmProfdata, LlvmReadobj, llvm_ar, llvm_as, llvm_bcanalyzer, llvm_dis,
llvm_filecheck, llvm_nm, llvm_objcopy, llvm_objdump, llvm_profdata, llvm_readobj, llvm_dwarfdump, llvm_filecheck, llvm_nm, llvm_objcopy, llvm_objdump, llvm_profdata,
llvm_readobj,
}; };
pub use crate::external_deps::python::python_command; pub use crate::external_deps::python::python_command;
pub use crate::external_deps::rustc::{self, Rustc, bare_rustc, rustc, rustc_path}; pub use crate::external_deps::rustc::{self, Rustc, bare_rustc, rustc, rustc_path};

View File

@@ -28,7 +28,17 @@ static C_NEVER_INLINED_PATTERN: &'static str = "bl.*<c_never_inlined>";
static C_NEVER_INLINED_PATTERN: &'static str = "call.*c_never_inlined"; static C_NEVER_INLINED_PATTERN: &'static str = "call.*c_never_inlined";
fn main() { fn main() {
test_lto(false);
test_lto(true);
}
fn test_lto(fat_lto: bool) {
let lto = if fat_lto { "fat" } else { "thin" };
let clang_lto = if fat_lto { "full" } else { "thin" };
println!("Running {lto} lto");
rustc() rustc()
.lto(lto)
.linker_plugin_lto("on") .linker_plugin_lto("on")
.output(static_lib_name("rustlib-xlto")) .output(static_lib_name("rustlib-xlto"))
.opt_level("2") .opt_level("2")
@@ -36,30 +46,36 @@ fn main() {
.input("rustlib.rs") .input("rustlib.rs")
.run(); .run();
clang() clang()
.lto("thin") .lto(clang_lto)
.use_ld("lld") .use_ld("lld")
.arg("-lrustlib-xlto") .arg("-lrustlib-xlto")
.out_exe("cmain") .out_exe("cmain")
.input("cmain.c") .input("cmain.c")
.arg("-O3") .arg("-O3")
.run(); .run();
let dump = llvm_objdump().disassemble().input("cmain").run();
// Make sure we don't find a call instruction to the function we expect to // Make sure we don't find a call instruction to the function we expect to
// always be inlined. // always be inlined.
llvm_objdump() dump.assert_stdout_not_contains_regex(RUST_ALWAYS_INLINED_PATTERN);
.disassemble()
.input("cmain")
.run()
.assert_stdout_not_contains_regex(RUST_ALWAYS_INLINED_PATTERN);
// As a sanity check, make sure we do find a call instruction to a // As a sanity check, make sure we do find a call instruction to a
// non-inlined function // non-inlined function
llvm_objdump() #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
.disassemble() dump.assert_stdout_contains_regex(RUST_NEVER_INLINED_PATTERN);
.input("cmain") #[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
.run() {
.assert_stdout_contains_regex(RUST_NEVER_INLINED_PATTERN); if fat_lto {
clang().input("clib.c").lto("thin").arg("-c").out_exe("clib.o").arg("-O2").run(); // fat lto inlines this anyway
dump.assert_stdout_not_contains_regex(RUST_NEVER_INLINED_PATTERN);
} else {
dump.assert_stdout_contains_regex(RUST_NEVER_INLINED_PATTERN);
}
}
clang().input("clib.c").lto(clang_lto).arg("-c").out_exe("clib.o").arg("-O2").run();
llvm_ar().obj_to_ar().output_input(static_lib_name("xyz"), "clib.o").run(); llvm_ar().obj_to_ar().output_input(static_lib_name("xyz"), "clib.o").run();
rustc() rustc()
.lto(lto)
.linker_plugin_lto("on") .linker_plugin_lto("on")
.opt_level("2") .opt_level("2")
.linker(&env_var("CLANG")) .linker(&env_var("CLANG"))
@@ -67,14 +83,13 @@ fn main() {
.input("main.rs") .input("main.rs")
.output("rsmain") .output("rsmain")
.run(); .run();
llvm_objdump()
.disassemble() let dump = llvm_objdump().disassemble().input("rsmain").run();
.input("rsmain") dump.assert_stdout_not_contains_regex(C_ALWAYS_INLINED_PATTERN);
.run() if fat_lto {
.assert_stdout_not_contains_regex(C_ALWAYS_INLINED_PATTERN); // fat lto inlines this anyway
llvm_objdump() dump.assert_stdout_not_contains_regex(C_NEVER_INLINED_PATTERN);
.disassemble() } else {
.input("rsmain") dump.assert_stdout_contains_regex(C_NEVER_INLINED_PATTERN);
.run() }
.assert_stdout_contains_regex(C_NEVER_INLINED_PATTERN);
} }

View File

@@ -0,0 +1,13 @@
#![allow(internal_features)]
#![feature(no_core, lang_items)]
#![no_core]
#![crate_type = "rlib"]
#[lang = "pointee_sized"]
trait PointeeSized {}
#[lang = "meta_sized"]
trait MetaSized: PointeeSized {}
#[lang = "sized"]
trait Sized: MetaSized {}
pub fn foo() {}

View File

@@ -0,0 +1,11 @@
#![allow(internal_features)]
#![feature(no_core, lang_items)]
#![no_core]
#![crate_type = "cdylib"]
extern crate lib;
#[unsafe(no_mangle)]
pub fn bar() {
lib::foo();
}

View File

@@ -0,0 +1,25 @@
// Compile a library with lto=fat, then compile a binary with lto=thin
// and check that lto is applied with the library.
// The goal is to mimic the standard library being build with lto=fat
// and allowing users to build with lto=thin.
//@ only-x86_64-unknown-linux-gnu
use run_make_support::{dynamic_lib_name, llvm_objdump, rustc};
fn main() {
rustc().input("lib.rs").opt_level("3").lto("fat").run();
rustc().input("main.rs").panic("abort").opt_level("3").lto("thin").run();
llvm_objdump()
.input(dynamic_lib_name("main"))
.arg("--disassemble-symbols=bar")
.run()
// The called function should be inlined.
// Check that we have a ret (to detect tail
// calls with a jmp) and no call.
.assert_stdout_contains("bar")
.assert_stdout_contains("ret")
.assert_stdout_not_contains("foo")
.assert_stdout_not_contains("call");
}

View File

@@ -0,0 +1,6 @@
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
define void @ir_callee() {
ret void
}

View File

@@ -0,0 +1,22 @@
#![allow(internal_features)]
#![feature(no_core, lang_items)]
#![no_core]
#![crate_type = "cdylib"]
#[lang = "pointee_sized"]
trait PointeeSized {}
#[lang = "meta_sized"]
trait MetaSized: PointeeSized {}
#[lang = "sized"]
trait Sized: MetaSized {}
extern "C" {
fn ir_callee();
}
#[no_mangle]
extern "C" fn rs_foo() {
unsafe {
ir_callee();
}
}

View File

@@ -0,0 +1,33 @@
// Check that -C lto=fat with -C linker-plugin-lto actually works and can inline functions.
// A library is created from LLVM IR, defining a single function. Then a dylib is compiled,
// linking to the library and calling the function from the library.
// The function from the library should end up inlined and disappear from the output.
//@ only-x86_64-unknown-linux-gnu
//@ needs-rust-lld
use run_make_support::{dynamic_lib_name, llvm_as, llvm_objdump, rustc};
fn main() {
llvm_as().input("ir.ll").run();
rustc()
.input("main.rs")
.opt_level("3")
.lto("fat")
.linker_plugin_lto("on")
.link_arg("ir.bc")
.arg("-Zunstable-options")
.arg("-Clinker-features=+lld")
.run();
llvm_objdump()
.input(dynamic_lib_name("main"))
.arg("--disassemble-symbols=rs_foo")
.run()
// The called function should be inlined.
// Check that we have a ret (to detect tail
// calls with a jmp) and no call.
.assert_stdout_contains("foo")
.assert_stdout_contains("ret")
.assert_stdout_not_contains("call");
}