Repository navigation
Tracking issue for MIR-only RLIBs #38913
Copy link
Copy link
Closed
Labels
A-MIRArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlA-codegenArea: Code generationArea: Code generationC-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFCCategory: An issue tracking the progress of sth. like the implementation of an RFCT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.Relevant to the compiler team, which will review and decide on the PR/issue.
Description
There's been some talk about switching RLIBs to "MIR-only", that is, make RLIBs contain only the MIR representation of a program and not the LLVM IR and machine code as they do now. This issue will try to collect some advantages, disadvantages, and other concerns such an approach would entail:
Advantages
- Less code duplication, which has four benefits:
- RLIBs would be smaller because they would not contain LLVM IR and machine code anymore.
- RLIBs and leaf crates would be smaller because, at the moment, instantiations of generic functions show up multiple times in the object code and LLVM IR.
- RLIBs and leaf crates would be smaller because the compiler would be able instantiate monomorphic functions strictly on demand, as @japaric points out.
- Possibly faster whole-project compiles, since generic instances are never compiled multiple times (although see "Disadvantages")
- RLIBs would compile faster because the trans and LLVM passes would always be skipped (much like when compiling with
-C metadata). - At the moment
libstdis compiled with-Cdebuginfo=1, which is good in general but as a side-effect increases the size of Rust binaries, even if they are built without debuginfo (because the debuginfo fromlibstdgets statically linked into the binaries). This problem would not exist with MIR-only rlibs. - In the past we've had problems with WeakODR linkage and COMDAT sections on MinGW. WeakODR linkage is one way to deal with duplicate generic instances and avoiding those would also remove any reason to use WeakODR.
- We would always get LTO-grade compiler optimizations since all code is available at codegen time.
- Some targets, like NVPTX, don't seem to support regular linking (see NVPTX: non-inlined functions can't be used cross crate #38787). Only generating object code in leaf crates would solve this problem.
- There seems be some indication that MIR-only RLIBs would help with making the Rust compiler more backend agnostic (see WASM-related issue Migrate wasm target to LLVM wasm backend #38804).
- Generating LLVM IR only in leaf crates would make it easier to add comprehensive LLVM-based instrumentation like LeakSanitizer without recompiling
libstd(see LeakSanitizer, ThreadSanitizer, AddressSanitizer and MemorySanitizer support #38699), as @japaric points out. - All Rust code (even that from
libstd) can be compiled with-C target-cpu=native, potentially resulting in better code, as @japaric points out. - The build process of multi-crate project would gain more parallelism, since downstream crates don't need to wait for upstream crate's codegen, even though they could already compile up until the linking phase, as @est31 points out.
Disadvantages
- The leaf crates (executables, staticlibs, dylibs, cdylibs) would take more time to compile because
- the machine code of monomorphic functions from upstream crates would not be "cached" anymore, and
- since LLVM sees more code at once, some super-linear optimizations would take dis-proportionally more time (like when one compiles with LTO now)
- People might rely on
pub #[no_mangle]items being exported from RLIBs and link against them directly. This would not be possible anymore, as @nagisa points out.
Non-Advantages
- MIR-only libs would not be platform independent. One could think that that should be the case but because of
cfgswitches, MIR is not platform independent either.
Mitigation strategies for disadvantages:
- The problem of caching machine code would be solved in a generalized form by incremental compilation. One has to keep in mind though that incremental compilation will produce less performant code because it prevents many opportunities for inlining.
- We could provide an additional, more coarse-grained codegen unit partitioning scheme for incremental compilation (e.g. one CGU per crate) for better runtime performance at the cost of longer compile times.
- The amount of code LLVM sees at once can easily be controlled via
-C codegen-unitsalready, which provides a means of reducing super-linear optimizations.
Open Questions
- I think we support "bundling" native libraries into RLIBs. We might still need to keep supporting this, even if we don't store machine code originating from Rust?
Please help collect more data on the viability of MIR-only RLIBs.
cc @rust-lang/core @rust-lang/compiler @rust-lang/tools @rkruppe
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
A-MIRArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlA-codegenArea: Code generationArea: Code generationC-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFCCategory: An issue tracking the progress of sth. like the implementation of an RFCT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.Relevant to the compiler team, which will review and decide on the PR/issue.
Activity
nagisa commentedon Jan 8, 2017
This is also potentially breaking people who are linking to rlibs expecting them to at least expose the extern
#[no_mangle]functions like it does currently.I did that at least once before, though the application where I did it was already very hacky for other reasons and I do not think the project is around anymore.
japaric commentedon Jan 8, 2017
Another advantage I see is that pure MIR RLIBs effectively let us "recompile"
stdwith different codegen options without needing std-aware Cargo or Xargo.This is assuming that the
stdcomponent thatrustupinstalls will also bepure MIR.
Basically,
cargo rustc --release -- -C target-cpu=nativewould optimizestdfor the host CPU "on the fly". Today, this requires using Xargo to recompile
std(i.e.RUSTFLAGS="-C target-cpu=native xargo build").Other case where one uses Xargo to recompile the
stdis producing anexecutable that aborts on
panic!s without the overhead of landing pads (thestdcomponent thatrustupinstall contains landing pads because it'scompiled with
-C panic=unwind). With pure MIR RLIBs, after you setpanic = "abort"in yourCargo.toml,cargo buildwill give you an executable thatdoesn't contain landing pads (everything would get compiled with
-C panic=abort).cc @brson ^ pure MIR RLIBs would eliminate the need for std-aware Cargo and
Xargo for some scenarios.
I expect the above will also make using sanitizers (cf #38699)
straightforward. Using a sanitizer requires (re)compiling everything with an
extra LLVM pass and linking to the sanitizer runtime, which is written in C/C++.
With pure MIR RLIBs using a sanitizer would become as simple as
cargo rustc -- -Z sanitizer=address; that would compile everything, includingstd, with theextra LLVM pass and also link the runtime, which would be provided as an e.g.
librustc_asan.rlibin thestdcomponent.cc @alexcrichton ^ relevant to sanitizer support
This would be required for the "easy sanitizers" scenario I'm describing above.
Or we could ship the sanitizers as "static libraries", i.e.
librustc_asan.a.Also, note that today one can build statically linked Rust programs using the
MUSL targets without needing to have MUSL installed because
libc.aisembedded inside the
stdrlib (libstd.rlib) that ships with thestdcomponent.
nagisa commentedon Jan 8, 2017
@japaric that’s trickly advantage as it prevents us from adding any MIR optimisations that depend on the codegen options set :) We already have one which acts upon the
-Zno-landing-pads(ergo--panic=abort)est31 commentedon Jan 8, 2017
@nagisa @japaric isn't platform independence listed in the issue description as non advantage?
I'd add to the advantages that it would add more parallelism, as the passes up to MIR being finished take less time than passes up to codegen being finished, and in combination with
codegen-units, you can now compile the code more in parallel than before. E.g. right now when I bootstrap the compiler, the "whole world" waits for the rustc crate to compile in a single thread. With the change, we wait less, as only its mir has to be available before we can continue. Afterwards when doing the codegen for the binary, we simply can use codegen-units to get the maximum amount of parallelism the hardware gives us.japaric commentedon Jan 8, 2017
Could you elaborate on how it would prevent you from adding such optimizations? The way I see it is that the
stdcomponent will probably continue to be compiled with-C panic=unwindso if you then compile your app with-C panic=abortthen LLVM won't be able to optimize as well (or as fast) as if you had recompiledstdwith-C panic=abortbecause of the MIR optimizations you mention. However, we would still be better off than today where thestdcomponent is shipped filled with landing pads. Or does LLVM always emit landing pads everywhere if the MIR "optimization" you mention is not present? (In that case, it no longer sounds like an optimization but more like a requirement)If you want the most optimized code possible then, yeah, you would have to use Xargo or std-aware Cargo to opt into MIR optimizations that depend on codegen options. While you are at it you can also throw in
--mir-opt-level=3, etc.est31 commentedon Jan 8, 2017
I agree that MIR optimisations don't really prevent you to have platform agnostic MIR. As both their input and output is MIR, those optimisations could be run in the leaf crates, once the target and other info is known.
However, if earlier stages in the compiler depend on the target, which is the case with cfg, one would either have to refactor the entire compiler to understand cfg's in all later stages, or simulate compilation with all possible combinations of cfg's enabled/disabled (in the end cfg is an on/off question). The first approach will probably hugely bloat code complexity of the compiler, the second approach would bloat runtime complexity exponentially by the number of kinds of used cfg's.
So MIR will probably stay platform dependent for some time.
japaric commentedon Jan 8, 2017
@est31
I'm not sure what are trying to get at? The
-C target-cpu=nativeoptimizations I'm referring to are about LLVM having access to the IR of all functions so it can apply autovectorization, CPU scheduling optimizations, etc. Whereas today-C target-cpu=nativeis not as good becauselibstd.rlibalready contains machine code that was optimized with-C target-cpu=generic. All these optimizations are "within an architecture", e.g. x86, and after e.g.cfg(target_arch)has taken effect so I'm not sure how "platform agnostic MIR" is relatednagisa commentedon Jan 8, 2017
@japaric Removing landing pads from MIR is already somewhat a problem since you cannot add them back after fact, so you already lose some of the so-called advantage by being unable to reverse that. Later on we might want to add something more invasive. For a completely hypothetical example consider something resembling autovectorisation which, again, is not exactly reversible and thus
-C target-feature=-stuffwould become no-op as well.-C debuginfo=2? Stripped to keep binaries smaller because of-C debuginfo=0before.-C debug-assertions? No-op even without MIR optimisations as debug assertions is essentially a#[cfg].So, what I’m trying to say is that specifying codegen options on leaf crates only would still not be equivalent (and diverge more over time with extra hypothetical MIR opts) to specifying the codegen option(s) for every crate.
You could (as @est31 did just now) argue for storing unoptimised MIR instead, but that, in addition to inreasing size of intermediate rlibs, serializes MIR opts.
Codegen options aren’t exactly related to platform independence in this context.
japaric commentedon Jan 8, 2017
@michaelwoerister
I'm not sure if this can be listed as an advantage but pure MIR RLIBs would have prevented #38824. The TL;DR is that LLVM raises assertions when you try lower functions that take/return
i128values to PTX code / MPS430 instructions because of bugs in LLVM. With pure MIR RLIBs I expect that if the leaf crate doesn't make use ofi128at all then those functions that usei128would never be fed into LLVM thus the LLVM assertions wouldn't have been triggered. I suppose that would be some sort of "dead code elimination" pass at the MIR level. So, basically less IR could be fed into LLVM with the right analysis.est31 commentedon Jan 8, 2017
@japaric
Ah, sorry, I've misread, you only talked about codegen options.
eddyb commentedon Jan 8, 2017
cc @solson @oli-obk (miri develooers)
michaelwoerister commentedon Jan 8, 2017
Great points @nagisa, @japaric, and @est31! I've added all of them to the list.
solson commentedon Jan 8, 2017
I don't think it will affect const evaluation one way or another, but it would help us test Miri outside rustc to be able to easily build dependencies as MIR-only rlibs (with MIR for all items, not just generic/inline/constant ones like in the existing metadata).
Largely, Miri is just like another backend in this context, so it is an instance of this previously mentioned advantage:
14 remaining items
oli-obk commentedon Aug 14, 2017
I have been getting undefined references to statics inside functions that were inlined into other crates when compiling libstd via xargo with
-Zalways-encode-mir -Zmir-opt-level=3. I have done some digging, but didn't get to the root.hanna-kruppe commentedon Aug 14, 2017
I believe I found yet another benefit to MIR-only rlibs: Currently,
cargo checkbuilds the entire dependency graph with--emit metadataonly, to avoid running translation. The downside is that the output of--emit metadata(*.rmetafiles) is sufficently different from normal (rlib) outputs that you need to rebuild the whole dependency graph when it's time tocargo build(and conversely, if you have a full build and runcargo check, all rmeta files are generated from scratch). This duplicates compilation effort and metadata on disk.With MIR-only rlibs, the only remaining differences between rlibs and rmeta files would (1) the rlib has wrapped the metadata in an archive file, and (2) the archive includes bundled native libraries, if any. Creating the archive should have neglegible cost, so we could probably get rid of rmeta files and make
--emit metadatabehave like--emit linkfor non-leaf-crates. It would still need to avoid running codegen in leaf crates (so it's not quite an alias) but it would greatly reduce the aforementioned duplication.(Going further, @nagisa (I think?) once suggested to me on IRC that metadata and machine code should be two separate files on disk. I found this appealing for other reasons, but to stay on-topic, such a split would make it possible to pick up a previously-generated rmeta file and generate all the machine code and so on from it, without recompiling the leaf crate from scratch. But that is mostly orthogonal to MIR-only rlibs, so whatever.)
hanna-kruppe commentedon Aug 14, 2017
@oli-obk re: the undefined references to statics: I don't have much time to investigate, but one cuplrit I can think of would be
internalize_symbols. Specifically, if a (non-pub)staticis apparently only used from within the CGU it's defined in, it is marked as internal and LLVM will remove it if it's not accessed anywhere in that CGU.Anyway, it would be great if you could file an issue for that (if there isn't one already) with a small test case. This is definitely a bug, but so far I don't believe it's an issue with statics getting translated locally.
aep commentedon Sep 22, 2017
uuh i'm scratching my head what i'm missing here: how is this gong to work with -C linker= ? We're relying on the fact that the hash of the input file to the linker is the same every invocation. If the objects get translated to native code before being passed to the linker, is the translation stable? Or will the targets system linker actually only ever see a single already relocated and re-ordered object file ?
hanna-kruppe commentedon Sep 22, 2017
This issue only affects which Rust code (or monomorphization of generic Rust code) gets translated into which LLVM compilation unit. It doesn't affect what happens afterwards with these LLVM modules, the resulting object files, etc. — and while it's plausible that MIR-only rlibs would enable more innovation in the later stages of the backend, nothing along those lines has been proposed or even discussed as far as I remember.
hanna-kruppe commentedon Oct 6, 2017
Another (marginal) benefit, assuming
#[inline]stops copying function bodies into multiple codegen units as discussed in the context of #44941:#[inline]becomes less necessary (only addsinlinehintinstead of enabling inlining at all in certain cases) and less complicated (easier to explain, easier to tell if it's useful).michaelwoerister commentedon Mar 1, 2018
I've put together a proof-of-concept implementation of this in #48373. Although the implementation crashes for many crates, I was able to collect timings for a number of projects. The tables show the aggregate time spent for various tasks while compiling the whole crate graph. In many cases we do less work overall but due to worse parallelization, wall-clock time increases. I.e. everything seems to be bottlenecked on the MIR-to-LLVM translation in the leaf crates. To me this suggests that MIR-only RLIBs are blocked on the compiler internals being parallelized.
ripgrep - cargo build
encoding-rs - cargo test --no-run
webrender - cargo build
futures-rs - cargo test --no-run
tokio-webpush-simple - cargo build
Number of LLVM function definitions generated for whole crate graph
eddyb commentedon Sep 7, 2018
What if we did this, but for
libcore...libstd, at stage1? It might be worth it, despite the huge number of tests, and should be a huge improvement when running just a few tests.(prompted by @dwijnand's comments on Discord about their workflow of changing
librustcand re-checking only one test - with incremental, most of the time is spent buildinglibcore...libstd)EDIT: here's some data, since I wanted to replicate what @dwijnand was seeing:
./x.py check src/libstd)touch src/librustc/lib.rs:stage1/bin/rustc- until Enable ThinLTO with incremental compilation. #53673 reaches beta)Most of the time is spent building libstd, which should be improved once #53673 ends up in beta (perhaps at the cost of the
rustcbuild time?), so the performance impact of using MIR-only rlibs might become less significant - we'll have to wait and see, I suppose.jyn514 commentedon Nov 10, 2020
Now that cargo passes
--embed-bitcode=no, is there anything left to do for this?jyn514 commentedon Nov 10, 2020
It turns out I was confused - this issue is about never going through LLVM at all, while
--embed-bitcode=noinstead embeds the object code generated by LLVM.https://rust-lang.zulipchat.com/#narrow/stream/122651-general/topic/How.20to.20learn.20more.20about.20crate.20metadata.3F/near/216169138
pnkfelix commentedon Mar 18, 2022
Visiting during T-compiler backlog bonanza meeting. We decided that this is at best S-tracking-unimplemented, but it also just represents an experimental idea that was being floated around, not something that anyone committed to.
Thus, it doesn't deserve a tracking issue: It deserves a separate MCP at this point.
Closing.