Summary
Allow Rust library to provide a simple hint about the minimum opt-level to build them with.
Motivation
When people build Rust projects, they have a choice of tradeoffs between the speed of the build and the performance of the code at runtime. Builds using the dev profile aim for minimal build time at the expense of unoptimized code at runtime; builds using the release profile spend more time building in order to improve runtime performance.
However, there are some library crates for which almost every use case wants
optimization, because an unoptimized build provides so little performance as to
be unusable. Such libraries typically include a note in their README.md
warning users to turn on optimization even in the dev profile:
[profile.dev.package."example-high-performance-package"]
opt-level = 3
This RFC adds a mechanism for crates to hint that builds should use optimization by default, while still allowing the top-level crate to easily override this. Handling this in dependencies makes it automatic for users, and also makes it easier to keep up with an evolving dependency tree.
Guide-level explanation
Some libraries benefit strongly from optimization, or are hurt especially by
builds without optimization, to the point that they expect almost no users to
ever want to run them unoptimized. Such libraries can use the Cargo hints
system to provide a minimum optimization level:
[hints]
min-opt-level = 2
When building with a profile with a default optimization level lower than the
hints.min-opt-level value, the crate will be built with the specified minimum
optimization level instead. When building with a profile with a higher
default optimization level, that higher optimization level will take precedence.
This applies whether the optimization level is the default for the profile, or is set by the user explicitly for the profile:
[profile.dev]
opt-level = 1
# Does not override a higher hints.min-opt-level specified in a dependency.
To override the hinted minimum optimization level, the top-level crate can use profile overrides to set the opt-level. Any opt-level specified via profile overrides will take precedence over any hint, whether the profile override applies to a specific crate or to all dependencies:
[profile.dev.package."*"]
opt-level = 1
# Overrides hints.min-opt-level specified in a dependency.
[profile.dev.package."random-package"]
opt-level = 0
# Overrides hints.min-opt-level specified in a dependency.
Note that setting opt-level = "s" or opt-level = "z" in the top-level
crate's profile will override min-opt-level in a dependency, on the theory
that crates optimizing for size are likely to want size-over-speed
optimizations in their dependencies as well.
Reference-level explanation
The hints.min-opt-level key requires an integer, and only supports numeric
hint levels (0, 1, 2, 3). Non-numeric hint levels like "s" and "z" are not
supported, because they don't fit into a strictly ordered progression, and
because they're more likely to be use-case-dependent and better determined by
the top-level crate.
hints.min-opt-level, like any hint, does not affect a crate's MSRV; older
versions of Cargo will ignore it.
Any profile override will take precedence over hint.min-opt-level, including
an override for all dependencies, an override for all build dependencies, or an
override for a specific dependency.
A profile specifying an opt-level will not override a higher
hints.min-opt-level specified in a dependency.
Note that a hint provided by a given library crate only applies to that specific crate, not that package's dependencies. If the code that needs optimizing is in a dependency, that dependency would need to add the hint.
There are two primary reasons to use this hint:
- The typical full build (not just an incremental build) will be sped up by optimizing the crate.
- The crate is disproportionately affected by optimization, such that it may be unusably slow (many times slower) without optimization.
Drawbacks
Crates could overuse this mechanism, requiring optimization even when they don't actually need it. We should provide clear documentation recommending when to use it and when not to use it.
Even when used for the intended purpose, different users may have different tradeoffs. The override mechanism allows users to retain control, but the defaults will not be ideal for all users. The mechanism proposed by this RFC is making a deliberate tradeoff, and proposes that the defaults will work better for more users than the current default of not providing these hints, but this RFC does not claim the new default will be an improvement for every user.
If a crate using this mechanism wishes to nonetheless build with different optimizations within its own workspace, it would have to add an override.
Limitations
Library crates cannot set this for dependencies they do not maintain; a crate can only set the min-opt-level for itself. This may cause issues for crates whose performance depends heavily on its dependencies; such crates may still have to rely on user documentation.
Library optimizations may not apply to code inlined or monomorphized by a user's crate.
Rationale and alternatives
This mechanism intentionally does not offer a "maximum optimization level", nor does it support optimizing for size.
This mechanism intentionally does not provide access to any specific target feature flags, as this is typically something the top-level crate needs to retain full control over based on its minimum system requirements.
We could support multiple min-opt-level hints, one for dev-like profiles and
one for release-like profiles, for crates that want a lower min-opt-level in
the dev profile. However, profile.release already defaults to opt-level = 3, so min-opt-level will generally never affect the release profile, only
the dev profile. Thus, a single setting seems sufficient.
We could have a simple boolean, e.g. optimize-in-dev = true, and leave it to
Cargo whether that means opt-level 1, 2, or 3. This would be simpler, but would
prevent crates from determining whether they benefit from opt-level 3 (e.g.
aggressive vectorization and loop unrolling) or not.
We could support "z" or "s" somehow. This would be more complex and require
more design, since those aren't on a linear scale like the numeric optimization
levels. Furthermore, these seem less likely to be usefully determined by
dependencies: whether a dependency will have massive performance issues if
built without optimization is something the dependency may know, while the need
for size optimization seems likely to be use-case-dependent and better set by
the top-level crate. (The use of crates designed for embedded, for instance,
does not necessarily indicate that the user is size-constrained and would
prefer size optimizations.)
We could have a hint apply recursively to dependencies. This seems like more control than a library crate could have, as a dependency may be used in multiple places in the crate graph.
Prior art
Cargo already provides the profile overrides mechanism, for the top-level crate to specify the opt-level of individual crates.
This RFC builds on the hints mechanism, currently used for
hints.mostly-unused. The
post announcing hints.mostly-unused
included
a section on future hints such as min-opt-level.
Cargo has
an issue discussing min-opt-level.
C and C++ compilers provide directives such as #pragma optimize or
__attribute__((optimize)), which let individual files or functions define an
optimization level.
Unresolved questions
Should a profile with opt-level = "s" or opt-level = "z"
override a dependency's min-opt-level? This RFC says it should,
but sometimes users might not want that. We could also give more
control over that.
Future possibilities
hints.min-opt-level is a simple mechanism providing a single hint. There are
many other possible optimization hints a library might wish to provide, and
we could consider adding further hints for those in the future. Any such hint
would need to balance the tradeoffs between value, additional complexity, and
whether crates in the ecosystem know the right optimization level for their
crate better than their users do.
We could in particular have a max-opt-level, for crates that don't benefit
from opt-level = 3 to lower the optimiation level to 2.
The mechanism to override a dependency's opt-level using profile.dev.package
forces a given opt-level whether the dependency asks for higher or lower. Users
of overrides, particularly those for "*", might want a mechanism for setting
a minimum without overriding a higher optimization level.
We may in the future want to change the default optimization level for the dev profile to 1, rather than 0. opt-level 1 includes optimizations that can make compilation faster, such as by sending less code to the codegen backend (e.g. LLVM). This might reduce the number of crates motivated to use this mechanism, but the mechanism would remain important, as there are library crates which strongly benefit from opt-level 2 or 3.
We may want to provide a further mechanism for libraries whose performance depends heavily on their dependencies to optimize those dependencies.
We may want to provide a mechanism for libraries to optimize code inlined or monomorphized by a user's crate. That would likely require compiler enhancements.
We may want to surface this in a visible way for users to see that it has been
applied to their dependencies. For instance, we could note it in --timings.