Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

-Clinker-plugin-lto doesn't work without extra manual work #60059

Open
glandium opened this issue Apr 17, 2019 · 19 comments
Open

-Clinker-plugin-lto doesn't work without extra manual work #60059

glandium opened this issue Apr 17, 2019 · 19 comments
Labels
A-linkage Area: linking into static, shared libraries and binaries O-macos Operating system: macOS T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@glandium
Copy link
Contributor

$ cargo new testcase
$ cd testcase
$ RUSTFLAGS="-Clinker-plugin-lto" cargo run --release

Yields the following on mac:

 
   Compiling testcase v0.1.0 (/Users/glandium/testcase)
error: linking with `cc` failed: exit code: 1
  |
  = note: "cc" "-m64" "-Wl,-plugin-opt=O3" "-Wl,-plugin-opt=mcpu=core2" "-L" "/Users/glandium/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib" "/Users/glandium/testcase/target/release/deps/testcase-d97266bc083c0e9e.testcase.4ank1dht-cgu.0.rcgu.o" "/Users/glandium/testcase/target/release/deps/testcase-d97266bc083c0e9e.testcase.4ank1dht-cgu.1.rcgu.o" "-o" "/Users/glandium/testcase/target/release/deps/testcase-d97266bc083c0e9e" "/Users/glandium/testcase/target/release/deps/testcase-d97266bc083c0e9e.3pvtkuuqiy7p6wge.rcgu.o" "-Wl,-dead_strip" "-nodefaultlibs" "-L" "/Users/glandium/testcase/target/release/deps" "-L" "/Users/glandium/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib" "/Users/glandium/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib/libstd-64d1544b9dc8a8d7.rlib" "/Users/glandium/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib/libpanic_unwind-47702365139f147e.rlib" "/Users/glandium/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib/libbacktrace_sys-0aefa3a2bfa44649.rlib" "/Users/glandium/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib/libunwind-c2b22c88cacffeb6.rlib" "/Users/glandium/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib/librustc_demangle-fca4484aa9be2d09.rlib" "/Users/glandium/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib/liblibc-4728c64ee20d89f8.rlib" "/Users/glandium/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib/liballoc-1722fbf72ce989c9.rlib" "/Users/glandium/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib/librustc_std_workspace_core-0836ff3f3d6a6ee6.rlib" "/Users/glandium/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib/libcore-91c9fbc323ad09b7.rlib" "/Users/glandium/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib/libcompiler_builtins-43b96ba2cdcc7cb3.rlib" "-lSystem" "-lresolv" "-lc" "-lm"
  = note: ld: unknown option: -plugin-opt=O3
          clang: error: linker command failed with exit code 1 (use -v to see invocation)
          

error: aborting due to previous error

error: Could not compile `testcase`.

To learn more, run the command again with --verbose.

On mac, the default linker is ld64. rustc really invokes cc, which invokes ld64 with the flags passed with -Wl and some others depending on the other command line arguments. ld64 does support the LLVM plugin... but doesn't support the -plugin-opt option to pass arguments to it. I know some things can be passed with -Wl,-mllvm,... but I don't know if that includes things that rust is trying to pass here.

On Linux, it's funnier:

   Compiling testcase v0.1.0 (/tmp/testcase)
error: linking with `cc` failed: exit code: 1
  |
  = note: "cc" "-Wl,--as-needed" "-Wl,-z,noexecstack" "-m64" "-Wl,-plugin-opt=O3" "-Wl,-plugin-opt=mcpu=x86-64" "-L" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib" "/tmp/testcase/target/release/deps/testcase-576934ab8201a4ea.testcase.dl00diw9-cgu.0.rcgu.o" "/tmp/testcase/target/release/deps/testcase-576934ab8201a4ea.testcase.dl00diw9-cgu.1.rcgu.o" "-o" "/tmp/testcase/target/release/deps/testcase-576934ab8201a4ea" "/tmp/testcase/target/release/deps/testcase-576934ab8201a4ea.y29oqvxl224jt9k.rcgu.o" "-Wl,--gc-sections" "-pie" "-Wl,-zrelro" "-Wl,-znow" "-Wl,-O1" "-nodefaultlibs" "-L" "/tmp/testcase/target/release/deps" "-L" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib" "-Wl,--start-group" "-Wl,-Bstatic" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libstd-bbd8cb236ab3b537.rlib" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libpanic_unwind-334e405e4bdf1791.rlib" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libbacktrace_sys-1e14a089a9f63178.rlib" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libunwind-f1aae4818bd13556.rlib" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/librustc_demangle-a32c94e7da1105b4.rlib" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/liblibc-e214e2acd110aec9.rlib" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/liballoc-fbf429991e30afee.rlib" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/librustc_std_workspace_core-1734308ff05fb551.rlib" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libcore-b349c8b817f959a5.rlib" "-Wl,--end-group" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libcompiler_builtins-c4b4b16c70e666d9.rlib" "-Wl,-Bdynamic" "-ldl" "-lrt" "-lpthread" "-lgcc_s" "-lc" "-lm" "-lrt" "-lpthread" "-lutil" "-lutil"
  = note: /tmp/testcase/target/release/deps/testcase-576934ab8201a4ea.testcase.dl00diw9-cgu.0.rcgu.o: file not recognized: file format not recognized
          collect2: error: ld returned 1 exit status
          

error: aborting due to previous error

error: Could not compile `testcase`.

To learn more, run the command again with --verbose.

Because cc is gcc, this just plain doesn't work, for no obvious reason. At the very least, it seems rust should try to use clang instead of cc in that case.

But that also fails:

RUSTFLAGS="-Clinker=clang-8 -Clinker-plugin-lto" cargo run --release
   Compiling testcase v0.1.0 (/tmp/testcase)
error: linking with `clang-8` failed: exit code: 1
  |
  = note: "clang-8" "-Wl,--as-needed" "-Wl,-z,noexecstack" "-m64" "-Wl,-plugin-opt=O3" "-Wl,-plugin-opt=mcpu=x86-64" "-L" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib" "/tmp/testcase/target/release/deps/testcase-e3dc53d119c25549.testcase.ci8upasg-cgu.0.rcgu.o" "/tmp/testcase/target/release/deps/testcase-e3dc53d119c25549.testcase.ci8upasg-cgu.1.rcgu.o" "-o" "/tmp/testcase/target/release/deps/testcase-e3dc53d119c25549" "/tmp/testcase/target/release/deps/testcase-e3dc53d119c25549.61wo3nlknhbro8d.rcgu.o" "-Wl,--gc-sections" "-pie" "-Wl,-zrelro" "-Wl,-znow" "-Wl,-O1" "-nodefaultlibs" "-L" "/tmp/testcase/target/release/deps" "-L" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib" "-Wl,--start-group" "-Wl,-Bstatic" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libstd-bbd8cb236ab3b537.rlib" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libpanic_unwind-334e405e4bdf1791.rlib" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libbacktrace_sys-1e14a089a9f63178.rlib" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libunwind-f1aae4818bd13556.rlib" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/librustc_demangle-a32c94e7da1105b4.rlib" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/liblibc-e214e2acd110aec9.rlib" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/liballoc-fbf429991e30afee.rlib" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/librustc_std_workspace_core-1734308ff05fb551.rlib" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libcore-b349c8b817f959a5.rlib" "-Wl,--end-group" "/home/glandium/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libcompiler_builtins-c4b4b16c70e666d9.rlib" "-Wl,-Bdynamic" "-ldl" "-lrt" "-lpthread" "-lgcc_s" "-lc" "-lm" "-lrt" "-lpthread" "-lutil" "-lutil"
  = note: /usr/bin/ld: bad -plugin-opt option
          clang: error: linker command failed with exit code 1 (use -v to see invocation)
          

error: aborting due to previous error

error: Could not compile `testcase`.

To learn more, run the command again with --verbose.

And here, the reason is essentially the same: the underlying linker doesn't support the -plugin-opt flag... except it does, but not when it's not passed -plugin, which happens when the compiler passes it, which happens when -flto was on its command line:

$ RUSTFLAGS="-Clinker=clang-8 -Clinker-plugin-lto -Clink-arg=-flto" cargo run --release   Compiling testcase v0.1.0 (/tmp/testcase)
    Finished release [optimized] target(s) in 0.28s
     Running `target/release/testcase`
Hello, world!

Using lld works too, because it doesn't need an explicit -plugin:

$ RUSTFLAGS="-Clinker=clang-8 -Clinker-plugin-lto -Clink-arg=-fuse-ld=lld" cargo run --release
   Compiling testcase v0.1.0 (/tmp/testcase)
    Finished release [optimized] target(s) in 0.36s
     Running `target/release/testcase`
Hello, world!
@Centril Centril added A-linkage Area: linking into static, shared libraries and binaries O-macos Operating system: macOS T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Apr 18, 2019
@froydnj
Copy link
Contributor

froydnj commented Apr 18, 2019

FWIW, you can pass an argument to -Clinker-plugin-lto to give it the name of the plugin to load, which would fix the clang case. (You could make an argument that said requirement is exactly the problem in the clang case.) The OS X and plain cc cases seem like out-and-out bugs, though.

@glandium
Copy link
Contributor Author

Indeed RUSTFLAGS="-Clinker=clang-8 -Clinker-plugin-lto=/usr/lib/llvm-8/lib/LLVMgold.so" cargo run --release works. It actually even works without -Clinker=clang-8.

@nikomatsakis
Copy link
Contributor

Nominating for (hopefully brief) discussion in the @rust-lang/compiler meeting -- this is holding up Firefox's efforts to use ThinLTO to eliminate glue code. It would be good to get it fixed or, at minimum, to specify what the correct behavior is and figure out who to ping about it (I'm not that familiar with these options).

@glandium
Copy link
Contributor Author

FWIW, we have a workaround for Firefox, which is that rust is not involved in linking at all. This is more about the general case than Firefox itself.

@nagisa
Copy link
Member

nagisa commented Apr 25, 2019

It seems that this issue would be mostly resolved by documenting which cases and linker/flag combinations we know to work. We could then see if there are any changes to the CLI that could be made to improve the experience here.

@nikomatsakis
Copy link
Contributor

Discussion from Zulip

@dwightguth
Copy link

I recently ran into this issue and wanted to mention that if you just want to have lto'd libraries that you are using to link an executable with clang, but can't build the rust code you need because crates that build binaries fail to link due to this error on mac OS, you can sidestep the issue by using the following script on the .rlib files generated by cargo build: https://github.com/kframework/llvm-backend/blob/6ec0694fe63f5c552ea3b64022ff8ba197d1252d/bin/llvm-kompile-rust-lto

It will not work if you want to link those libraries with rustc though (although it could be made to work with a small additional effort)

@michaelwoerister
Copy link
Member

Because cc is gcc, this just plain doesn't work, for no obvious reason. At the very least, it seems rust should try to use clang instead of cc in that case.

Should it though? I'm unclear as to what our policy is here? As a user it would make me a bit uneasy if a program like rustc would silently decide to use a different linker. Things would be different if we shipped our own LLVM linker plugin and passed that on to cc.

@vext01
Copy link
Contributor

vext01 commented Apr 20, 2021

Just wanted to mention that it's still quite difficult and unobvious to get a linker LTO plugin working with rustc.

I had expected to just pass -C linker_plugin_lto, but as others have noted, it's not that easy.

In case someone else is trying to get this working (including my future self), here's what I had to do (on Linux).

I couldn't see a way to get rustbuild to install all of the necessary llvm-related tools (namely lld and clang), so I ended up building my own. You can build them all directly from the llvm git repo. Once cloned I checked out the llvmorg-12.0.0 release tag and configured the build like this:

$ cmake -DCMAKE_INSTALL_PREFIX=/opt/llvm-12.0.0 \
    -DLLVM_INSTALL_UTILS=On \
    -DCMAKE_BUILD_TYPE=release \
    -DLLVM_ENABLE_PROJECTS="lld;clang" \
    ../llvm

After building and installing llvm, put its bin/ dir in your $PATH.

Then I built a stage 1 rust with this in config.toml:

[target.x86_64-unknown-linux-gnu]
llvm-config = "/opt/llvm-12.0.0/bin/llvm-config"

I added the resulting toolchain to rustup, and built a rust project like this:

$ RUSTFLAGS="-C linker_plugin_lto -C linker=clang -Clink-arg=-fuse-ld=lld" cargo +rust-stage1 build --release

(Note that rust, lld and clang must all be using the same version of LLVM IR. You can't mix tools built with different llvm versions)

It took me a few hours to figure all of this out, so I'm quite keen to either make this easier, or at least document better how to do it. Any thoughts?

@vext01
Copy link
Contributor

vext01 commented Apr 20, 2021

I've just learned that I should also have used a recent branch on rust's fork of LLVM, but the above instructions are otherwise still valid.

@danakj
Copy link
Contributor

danakj commented May 18, 2023

It looks like this was agreed on to be a bug for the MacOS case. We're now bumping into this in Chromium for any target where Rust drives the linking (anything with fn main() in it).

We want to use ld64.lld, but it doesn't handle the -plugin-opt arguments:

  = note: ld64.lld: error: unknown argument '-plugin-opt=O3'
          ld64.lld: error: unknown argument '-plugin-opt=mcpu=core2'

I don't see a workaround for this issue above, nor any fixes for it, did I miss it?

aarongable pushed a commit to chromium/chromium that referenced this issue May 18, 2023
Tools built to be used during the build process don't need PGO and
other official build things. This works around a bug on Mac for
chrome.exe where Rust gives the linker -plugin-opt flags it doesn't
understand: rust-lang/rust#60059

However the bug will be unresolved for other build targets that are
link-driven by Rust.

R=brucedawson@chromium.org

Bug: 1386212
Change-Id: I21824ad484048f0bc283454579f2f603acf3fa99
Cq-Include-Trybots: luci.chromium.try:win-rust-x64-rel,win-rust-x64-dbg,linux-rust-x64-rel,linux-rust-x64-dbg,android-rust-arm64-rel,android-rust-arm64-dbg,android-rust-arm32-rel
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4545355
Commit-Queue: danakj <danakj@chromium.org>
Reviewed-by: Bruce Dawson <brucedawson@chromium.org>
Auto-Submit: danakj <danakj@chromium.org>
Commit-Queue: Bruce Dawson <brucedawson@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1146226}
@danakj
Copy link
Contributor

danakj commented Jun 23, 2023

Bump - how does on enable LTO in a mixed binary that contains Rust and C++ and is linked by Rust on MacOS?

@danakj
Copy link
Contributor

danakj commented Jun 23, 2023

In case it's of help since the Zulip seems to suggest that things work if you use clang as your linker, we are using clang++ as the linker, with -Clinker-plugin-lto=yes and no other -Clink-arg to control the linker.

It seems that rustc needs to not pass -plugin-opt=O3 -plugin-opt=mcpu=core2 on MacOS.

aarongable pushed a commit to chromium/chromium that referenced this issue Jun 30, 2023
When linker plugin LTO is enabled on Apple, rustc passes invalid
command line arguments to the linker. They are valid elsewhere but
not known by the Apple linker.

Fortunately, on Apple we have a python script that is used to invoke
the linker. So we drop them from the command line in linker_driver.py
for now.

Upstream issue: rust-lang/rust#60059

R=hans@chromium.org

Bug: 1446796
Change-Id: Idc051457cff194616ac9b0063e3fd255fdd261cc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4659997
Reviewed-by: Adrian Taylor <adetaylor@chromium.org>
Commit-Queue: danakj <danakj@chromium.org>
Reviewed-by: Hans Wennborg <hans@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1164710}
UI-RayanWang pushed a commit to ubiquiti/ubnt_libjingle_component_src_build that referenced this issue Aug 16, 2023
Tools built to be used during the build process don't need PGO and
other official build things. This works around a bug on Mac for
chrome.exe where Rust gives the linker -plugin-opt flags it doesn't
understand: rust-lang/rust#60059

However the bug will be unresolved for other build targets that are
link-driven by Rust.

R=brucedawson@chromium.org

Bug: 1386212
Change-Id: I21824ad484048f0bc283454579f2f603acf3fa99
Cq-Include-Trybots: luci.chromium.try:win-rust-x64-rel,win-rust-x64-dbg,linux-rust-x64-rel,linux-rust-x64-dbg,android-rust-arm64-rel,android-rust-arm64-dbg,android-rust-arm32-rel
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4545355
Commit-Queue: danakj <danakj@chromium.org>
Reviewed-by: Bruce Dawson <brucedawson@chromium.org>
Auto-Submit: danakj <danakj@chromium.org>
Commit-Queue: Bruce Dawson <brucedawson@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1146226}
NOKEYCHECK=True
GitOrigin-RevId: df7d81e33033a594ff56d5ab8387aa1f13cd1c39
UI-RayanWang pushed a commit to ubiquiti/ubnt_libjingle_component_src_build that referenced this issue Oct 4, 2023
When linker plugin LTO is enabled on Apple, rustc passes invalid
command line arguments to the linker. They are valid elsewhere but
not known by the Apple linker.

Fortunately, on Apple we have a python script that is used to invoke
the linker. So we drop them from the command line in linker_driver.py
for now.

Upstream issue: rust-lang/rust#60059

R=hans@chromium.org

Bug: 1446796
Change-Id: Idc051457cff194616ac9b0063e3fd255fdd261cc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4659997
Reviewed-by: Adrian Taylor <adetaylor@chromium.org>
Commit-Queue: danakj <danakj@chromium.org>
Reviewed-by: Hans Wennborg <hans@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1164710}
NOKEYCHECK=True
GitOrigin-RevId: dae22dc704135119ae08dabc84deaf39b74d8c6c
@cormacrelf
Copy link
Contributor

cormacrelf commented Nov 24, 2023

I have looked into this. Here's what I turned up:

Workarounds

  • On macOS, what rustc should pass to the linker for -Clinker-plugin-lto is... nothing. No flags. You're not missing out on anything. Apple's ld64 linker is already good to go. So you can omit it when building a binary crate on macOS.
  • You often don't need the full syntax -Clinker-plugin-lto="path/to/libLTO.dylib", either. This is the case if you're running a standard Apple clang toolchain that can find its own libLTO.dylib when it encounters bitcode.
  • I suspect most people in this issue are in the deep end somewhere, e.g. wanting to use a version of libLTO other than the one bundled with an old clang-8 toolchain, or wanting to use a linker that doesn't come as part of a toolchain. E.g. the sold linker, or a standalone build of LLD.
  • Cargo does not have this problem support deferring optimisation to the linker. Building using a Cargo.toml like
    [profile.release]
    lto = "thin"
    
    will do the LTO part inside rustc, using rustc's bundled LLVM, before it hits the linker. This is because it only includes -Clinker-plugin-lto when building library crates.
  • If you're working with a non-cargo build tool, then you can probably make a workaround by similarly omitting -Clinker-plugin-lto for rust binaries, passing -Clink-args="-lto_library path/to/libLTO.dylib directly if you need it. On linux, you can just pass -Clinker-plugin-lto, probably.

Actions to take

  • We should fix these bugs, so that you can use -Clinker-plugin-lto on macOS, as rustc is supposed to support. That functionality is only useful when rustc calls the linker. It should work on macOS, because ld64 has enough flags for it. And it shouldn't emit GNU-only flags on macOS.
  • When the linker flags are a bit more solid, maybe Cargo should start passing -Clinker-plugin-lto for binaries. (Cargo doesn't support cross-language LTO without setting RUSTFLAGS, because its default configuration only uses system-provided linkers and they can't hack it.)

Where this problem comes from

  • The -plugin= and -plugin-opt=... flags were implemented first in gold. LLD emulates them in GNU mode. ld64 does not.
  • This is where rustc emits the GNU-specific -plugin-opt=... flags
  • The code was added in 2018, in a981089
  • A year or so later, the same author Michael Woerister summarised the work of getting cross-language LTO working in this article
  • Because Cargo doesn't use -Clinker-plugin-lto when linking, virtually nobody has tested it in the last 5 years.
  • Misc: There are some docs from LLVM about how to turn various ThinLTO knobs for various linkers, notably including incremental caching for ThinLTO. This is perhaps of interest to linking on the scale of chromium, or anyone using bazel/buck2/pants etc.

ld64's LTO flags

Here are the docs for `ld64` relating to lto, from the manpage:
     -object_path_lto filename
             When performing Link Time Optimization (LTO) and a temporary
             mach-o object file is needed, if this option is used, the
             temporary file will be stored at the specified path and remain
             after the link is complete.  Without the option, the linker picks
             a path and deletes the object file before the linker tool
             completes, thus tools such as the debugger or dsymutil will not
             be able to access the DWARF debug info in the temporary object
             file.

     -lto_library path
             When performing Link Time Optimization (LTO), the linker normally
             loads libLTO.dylib relative to the linker binary
             (../lib/libLTO.dylib). This option allows the user to specify the
             path to a specific libLTO.dylib to load instead.

     -cache_path_lto path
             When performing Incremental Link Time Optimization (LTO), use
             this directory as a cache for incremental rebuild.

     -prune_interval_lto seconds
             When performing Incremental Link Time Optimization (LTO), the
             cache will pruned after the specified interval. A value 0 will
             force pruning to occur and a value of -1 will disable pruning.

     -prune_after_lto seconds
             When pruning the cache for Incremental Link Time Optimization
             (LTO), the cache entries are removed after the specified
             interval.

     -max_relative_cache_size_lto percent
             When performing Incremental Link Time Optimization (LTO), the
             cache will be pruned to not go over this percentage of the free
             space. I.e. a value of 100 would indicate that the cache may fill
             the disk, and a value of 50 would indicate that the cache size
             will be kept under the free disk space.
  • ld64 has a bug where it cannot handle symlinks in -lto_library symlink/to/libLTO.dylib. You must use a real path, although it can be relative or absolute. LLD fixed a similar bug with -plugin= in about 2016, but ld64 still suffers. This would apply to -Clinker-plugin-lto="path/to/libLTO.dylib", if it were implemented for darwin linkers. But rustc currently always uses GNU-style args (-plugin= instead of -lto_library).
  • LLD in darwin mode, AKA ld64.lld, seems to emulate ld64 pretty closely. Some resources on the internet say that ld64.lld is no good. I think it may be good now. Apparently Facebook's been working on it.
  • ld64 does not document this, but it supports -mllvm to pass args through to LLVM. I think these will be what you can pass: clang++ -mllvm -help -x c -c /dev/null -- i.e. nothing much useful. One or two LTO knobs.

How cargo handles lto = "thin" in Cargo.toml

  • It only passes -Clinker-plugin-lto when building libraries. This does not translate to any linker flags -- rustc does not call the linker when building a library.
  • It does not pass -Clinker-plugin-lto when building a binary. This is when rustc calls the linker, so this does not result in any special LTO linker flags.
  • Therefore cargo does not exercise the code path that emits these -plugin-opt flags, on any platform. That code has been buggy for years but very few people were using rustc directly.
  • When building a binary, Cargo only passes -Clto=thin. That doesn't translate to any linker flags either -- rustc just generates a bunch of codegen units (.rcgu.o files) and passes them all to the linker. They happen to be bitcode files.

Given that the linker is being passed LLVM bitcode objects and is, in fact, doing LTO, you may wonder: how??

  • When linkers first see bitcode in an object file, they respond by loading the LLVM linker plugin (see e.g. ld64's manpage on its default -lto_library path, i.e. ../lib/libLTO.dylib relative to some directory... somehow ending up at /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libLTO.dylib or somewhere similar, probably.)

@bjorn3
Copy link
Member

bjorn3 commented Nov 24, 2023

This is because it only includes -Clinker-plugin-lto when building library crates, apparently on linux too.

It doesn't pass it to actually enable linker plugin LTO in that case, but to create an rlib which only contains bitcode rather than the default object code + bitcode hybrid rustc produces by default when LTO is enabled.

If you're working with a non-cargo build tool, then you can probably make a workaround by similarly omitting -Clinker-plugin-lto for rust binaries, passing -Clink-args="-lto_library path/to/libLTO.dylib directly if you need it. On linux, you can just pass -Clinker-plugin-lto, probably.

That will male rustc do the LTO instead in which case you won't get cross-language LTO at all. You need -Clinker-plugin-lto to actually make rustc forward the bitcode files to the linker for cross language LTO.

By the way the LLVM shipped with XCode is almost always too old to load rustc produced bitcode files, so if it seems to work without using the linker plugin bundled with rustc, you aren't actually doing linker plugin LTO.

@cormacrelf
Copy link
Contributor

cormacrelf commented Nov 24, 2023

If you're working with a non-cargo build tool, then you can probably make a workaround by similarly omitting -Clinker-plugin-lto for rust binaries, passing -Clink-args="-lto_library path/to/libLTO.dylib directly if you need it. On linux, you can just pass -Clinker-plugin-lto, probably.

That will male rustc do the LTO instead in which case you won't get cross-language LTO at all. You need -Clinker-plugin-lto to actually make rustc forward the bitcode files to the linker for cross language LTO.

I don't mean omitting it for cargo build -p bin_crate. I mean configuring rust library targets differently from rust binary ones, sending -Clinker-plugin-lto to the libraries (to emit bitcode .rlibs) but not to the binary targets, mirroring what cargo does by default for lto = "thin". But you might be configuring a rust toolchain in Bazel.

Also, correction: -Clink-arg can't do this. The -lto_library flag needs to be passed before all the .rlib and .o files. So rather:

# or your libLTO of choice. -v makes it verbose
( echo "#!/bin/sh";
  echo 'exec cc -Wl,-v -Wl,-lto_library,/nix/store/l1lgmvwm5vd08cs127r8s7y9xycsdh0i-llvm-16.0.6-lib/lib/libLTO.dylib "$@"';
) > cc-wrapper.sh
chmod +x cc-wrapper.sh
export RUSTFLAGS="-Clinker=$PWD/cc-wrapper.sh"
export RUSTC_LOG="rustc_codegen_ssa::back::link=info" # to get the linker's -v output on your screen
cargo build --release

# it spits out
@(#)PROGRAM:ld  PROJECT:dyld-1015.7
BUILD 18:48:48 Aug 22 2023
configured to support archs: ...
LTO support using: LLVM version 16.0.6 (static support for 29, runtime is 29)
TAPI support using: Apple TAPI version 15.0.0 (tapi-1500.0.12.3)
...

By the way the LLVM shipped with XCode is almost always too old to load rustc produced bitcode files, so if it seems to work without using the linker plugin bundled with rustc, you aren't actually doing linker plugin LTO.

FWIW, I just built rust-analyzer using rustc 1.74 + Cargo.toml lto="thin" + Xcode 15 LLVM (Apple clang "15.0.0" is Xcode 15, not clang 15). The .rlib files are bitcode only, according to nm target/release/deps/blah-XXX.rlib. It took ages to build the final rust-analyzer(bin) crate, as you would expect. As I understand it, this is linker plugin LTO.

Also, rustc does not ship a libLTO.dylib.

@bjorn3
Copy link
Member

bjorn3 commented Nov 24, 2023

As I understand it, this is linker plugin LTO.

If you didn't pass -Clinker-plugin-lto, rustc does the LTO on all rust code and then produces object files that are passed to the linker. The linker never sees any bitcode and thus can't apply LTO between rust code and C/C++ code.

@cormacrelf
Copy link
Contributor

Ohhh, you mean rustc will pick up the .rlibs, and optimise them internally with its own llvm. Hence a billion .rcgu.os from the linker. Got it.

matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Nov 28, 2023
…o-wasm, r=petrochenkov

Perform LTO optimisations with wasm-ld + -Clinker-plugin-lto

Fixes (partially) rust-lang#60059. Technically, `--target wasm32-unknown-unknown -Clinker-plugin-lto` would complete without errors before, but it was not producing optimized code. At least, it may have been but it was probably not the opt-level people intended.

Similarly to rust-lang#118377, this could benefit from a warning about using an explicit libLTO path with LLD, which will ignore it and use its internal LLVM. Especially given we always use lld on wasm targets. I left the code open to that possibility rather than making it perfectly neat.
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Nov 29, 2023
Rollup merge of rust-lang#118378 - cormacrelf:bugfix/linker-plugin-lto-wasm, r=petrochenkov

Perform LTO optimisations with wasm-ld + -Clinker-plugin-lto

Fixes (partially) rust-lang#60059. Technically, `--target wasm32-unknown-unknown -Clinker-plugin-lto` would complete without errors before, but it was not producing optimized code. At least, it may have been but it was probably not the opt-level people intended.

Similarly to rust-lang#118377, this could benefit from a warning about using an explicit libLTO path with LLD, which will ignore it and use its internal LLVM. Especially given we always use lld on wasm targets. I left the code open to that possibility rather than making it perfectly neat.
@Congyuwang
Copy link

Congyuwang commented Mar 1, 2024

According to LLVM@17 ld64.lld --help:

-lto_library <path> Obsolete. LLD supports LTO directly, without using an external dylib.

--lto-CGO<cgopt-level>  Set codegen optimization level for LTO (default: 2)
--lto-O<opt-level>      Set optimization level for LTO (default: 2)

-O <value>  Optimize output file size

So, for newer lld we can ignore these flags

"-Wl,-plugin=/opt/homebrew/opt/llvm/lib/libLTO.dylib" "-plugin-opt=mcpu=apple-m1"

and convert the -plugin-opt=O* to --lto-CGO* codegen opt level.

Temporary workaround:

macos-linker.sh

#!/bin/sh
# this is a wrapper to adapt ld64 to gnu style arguments

declare -a args=()
for arg in "$@"
do
    # options for linker
    if [[ $arg == "-Wl,"* ]]; then
        IFS=',' read -r -a options <<< "${arg#-Wl,}"
        for option in "${options[@]}"
        do
            if [[ $option == "-plugin="* ]] || [[ $option == "-plugin-opt=mcpu="* ]]; then
                # ignore -lto_library and -plugin-opt=mcpu
                :
            elif [[ $option == "-plugin-opt=O"* ]]; then
                # convert -plugin-opt=O* to --lto-CGO*
                args[${#args[@]}]="-Wl,--lto-CGO${option#-plugin-opt=O}"
            else
                # pass through other arguments
                args[${#args[@]}]="-Wl,$option"
            fi
        done

    else
        # pass through other arguments
        args[${#args[@]}]="$arg"
    fi
done

# use clang to call ld64
exec ${CC} -v "${args[@]}"

Build with the following ENVs:

CC=${HOMEBREW_PREFIX}/opt/llvm/bin/clang \
CXX=${HOMEBREW_PREFIX}/opt/llvm/bin/clang++ \
AR=${HOMEBREW_PREFIX}/opt/llvm/bin/llvm-ar \
CFLAGS="-flto=thin -O3" \
CXXFLAGS="-flto=thin -O3" \
RUSTFLAGS="-Clinker-plugin-lto -Clinker=$PWD/macos-linker.sh -Clink-arg=-fuse-ld=${HOMEBREW_PREFIX}/opt/llvm/bin/ld64.lld" \
cargo build --release

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-linkage Area: linking into static, shared libraries and binaries O-macos Operating system: macOS T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests