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

Make rustc and cargo produce optimized binaries by default #967

Closed
wants to merge 4 commits into from

Conversation

Valloric
Copy link

Make rustc and cargo produce optimized binaries by default.

Rendered.

@Valloric Valloric changed the title Adding opt-by-default RFC Make rustc and cargo produce optimized binaries by default Mar 11, 2015
@Valloric Valloric force-pushed the opt-by-def branch 2 times, most recently from 493406e to 32fad3e Compare March 11, 2015 03:21

While this alone would be a large safety & usability boost over the current
state, it would not help Rust newcomers or those playing around with/evaluating
the language. Such users are less likely to know about `cargo`, and even if they
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, our current docs use cargo from the get go (after "hello world"): http://doc.rust-lang.org/nightly/book/hello-cargo.html is the third chapter.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rest of that section explains:

and even if [the newbie users do know about cargo], they are likely to shun it while experimenting with simple programs. In such cases, they could easily conclude that "Rust is too slow" based on their initial experiments and never reach cargo build.

If I were coming to Rust today and wanted to play around with it before deciding is this worth my time or not, I wouldn't bother with cargo at all. rustc foo.rs produces a binary I can run "just fine" (or so it appears).

Why would anyone experimenting with Rust go through the trouble to create a Cargo.toml file? For what, a simple test program? This isn't a realistic expectation. They haven't yet been sold on the language to bother with something like that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I believe the docs point out the easy way to create a Cargo.toml file, what I don't buy is that all people playing around with/evaluating Rust will create one at all. Again, why would they? If the only thing you're doing is trying to get a grasp for the language syntax, some initial libs and maybe a feel for the perf overhead over C & C++ by writing a few simple algorithms, why go through the trouble of learning this new "cargo" tool and a "TOML" file and yadda, yadda, I just want to see how the language runs.

That's how I'd do it as a Rust newbie. Why spend any effort (no matter how small it is) to learn to use cargo when I can accomplish my task of (lightly) evaluating Rust with just rustc? Why spend any amount of effort above the strictly necessary?

Never underestimate programmer laziness.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I'm not sure that avoiding cargo is the least lazy path... it's certainly not as clear cut as it would be with, say, C++ (where getting more than just a compiler requires installing/compiling/wrangling things in possibly non-trivial ways).

The things that incline me to think otherwise are the facts that cargo is distributed alongside the compiler by default, the docs use cargo from the start, and the 'ease' of cargo new and cargo run.

That said, I personally do often write tiny test-cases etc. and compile them with just rustc. And the behaviour from many other languages is definitely to compile with fooc and then run the created binary (or just run with the foo interpreter), so the fact that cargo deviates from that probably means it is more effort.

In any case, I suspect that there will be "surprisingly" many newbies who have never run rustc manually, using play.rust-lang.org and cargo for everything.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In any case, I suspect that there will be "surprisingly" many newbies who have never run rustc manually, using play.rust-lang.org and cargo for everything.

Oh I don't doubt that, and good for them! :)

I also don't doubt that there will be an unsurprisingly large number of newbies who will spend a non-trivial amount of time with just rustc, at least at first (and long enough to form incorrect opinions about Rust perf).

@comex
Copy link

comex commented Mar 11, 2015

A serious problem with optimizing by default is that it removes integer overflow checks, so the assumption that people will generally test code with overflow checks enabled (because they'll use debug builds when developing) may fall down. You could otherwise optimize but keep them in, but rustc isn't really currently designed to minimize the performance impact of those checks...

Perhaps the least worst option is to (1) have cargo's echo actually use the word "unoptimized", rather than "debug", and (2) add an echo to rustc (:/), which could be disabled if the user specified any optimization level manually.

@rschmitt
Copy link

In principle, there doesn't need to be a default. Cargo and/or rustc could make the user explicitly specify an optimization level, either at the command line or in the Cargo.toml file. If an optimization level is not specified, that's the perfect time to spit out some documentation listing the choices and how they differ. I have no idea if this approach would actually be wise, but it could at least be worth mentioning in the Alternatives section.

@Valloric
Copy link
Author

A serious problem with optimizing by default is that it removes integer overflow checks, so the assumption that people will generally test code with overflow checks enabled (because they'll use debug builds when developing) may fall down.

The majority of all builds will continue to be debug builds because people will want debug symbols and a faster iteration cycle. Nothing about that changes with this RFC. The only thing that changes is that if you forget to specify what kind of build you want, you'll get the optimized build.

Newbies won't know that the debug/opt distinction exists so they'll start with opt-only builds but as they learn a bit more about Rust they'll start using debug builds more often (because of the above-listed reasons).

The point of this RFC is not to change the culture to using opt builds in development, it's to prevent users from shooting themselves in the foot.

@Valloric
Copy link
Author

In principle, there doesn't need to be a default. Cargo and/or rustc could make the user explicitly specify an optimization level, either at the command line or in the Cargo.toml file.

I'd be amenable to that, but it might be unnecessary complexity for newbies (and others). There's something to be said for "if I give a compiler a program file, I should get back a binary." Avoiding a reasonable, safe default and forcing a choice every time seems like a cop-out. We know what the safe default is, and it certainly isn't debug-by-default.

I have no idea if this approach would actually be wise, but it could at least be worth mentioning in the Alternatives section.

I agree; I'll add it to the alternatives section.

@comex
Copy link

comex commented Mar 11, 2015

@Valloric Good point, though I'm not sure the default won't have some effect on that culture in practice. Also, I think it may be confusing for newbies if integer overflow is supposed to panic yet they don't see this happen in practice in their first programs.

@Valloric
Copy link
Author

Good point, though I'm not sure the default won't have some effect on that culture in practice.

Since opt builds are substantially slower to produce and thus iterate on, I think people will quickly gravitate towards the --debug flag. Which is great, because at that point they'll know the difference between a debug and a release build, instead of assuming the only thing that exists is a fast-to-build-but-slow-to-run mode.

Also, I think it may be confusing for newbies if integer overflow is supposed to panic yet they don't see this happen in practice in their first programs.

I find it unlikely that newbies will encounter integer overflow in their initial programs. I've been writing C++ for 15 years and I still haven't seen it bite me. :)

@emberian
Copy link
Member

Optimizing and having debug assertions are completely separate axes of configuration. You can have both, and if we optimize by default, we should have both (outside of --release builds, I think, where the distinction would be disabling debug assertions).

@Valloric
Copy link
Author

Optimizing and having debug assertions are completely separate axes of configuration. You can have both, and if we optimize by default, we should have both

This is an excellent point. In the C++ world, lots of people run optimized builds with assertions turned on. In fact, all Google (C++) production code runs like this, and assertion statements (as macros) are incredibly widely used.

@killercup
Copy link
Member

Optimizing and having debug assertions are completely separate axes of configuration. You can have both, and if we optimize by default, we should have both

Personally, I'd like to see cargo build with optimizations and debug assertions by default. If compile time is a problem, you could add a flag (e.g. --quick or --no-opt) to build the current project (not the dependencies) without optimizations. Choosing a different profile (like --release) would overwrite these defaults.

@tbu-
Copy link
Contributor

tbu- commented Mar 11, 2015

I'm against making it the default. In my opinion, cargo should make it painfully visible that it's making an unoptimized build, and rustc should just continue doing what it does currently, for consistency with basically all other languages out there.

@mahkoh
Copy link
Contributor

mahkoh commented Mar 11, 2015

This was discussed in last weeks meeting: https://github.com/rust-lang/meeting-minutes/blob/master/weekly-meetings/2015-03-03.md#cargo-and-optimizations

I don't like the RFC as it is right now but I could get behind it if you add the following:


Add a configuration option optimize_by_default to the file ~/.config/rustc/config which will be read by rustc. The setting in this file can be overridden by an environment variable RUSTC_OPTIMIZE_BY_DEFAULT. If neither is set, it defaults to true.

@mahkoh
Copy link
Contributor

mahkoh commented Mar 11, 2015

And if we make the use of different optimization flags more common, then they should be called O0, O1, O2 = O , and O3.

@hauleth
Copy link

hauleth commented Mar 11, 2015

I would vote that rustc will compile with optimizations on, but cargo should remain as is, or even cargo build should be deprecated in favour of cargo debug and cargo release. This would prevent silly mistakes.

The optimizations in rustc should be enabled for newcomers and haters. Both of them need to see that Rust is fast with microbenchmarks and simple code for learning. The first ones will fast come to Cargo (lesson 2 after language basics) and second ones will be 💩ing all the time, no matter what. But at least we can shut their mouth about speed of executables.

@Valloric
Copy link
Author

This was discussed in last weeks meeting: https://github.com/rust-lang/meeting-minutes/blob/master/weekly-meetings/2015-03-03.md#cargo-and-optimizations

Thanks for pointing that out, I was unaware of it. I'd like to address some of the comments from the meeting minutes:

acrichto: Build times are really slow. When using Cargo, I want a developer-optimized default. I know as a developer that I should switch when I want to profile. So it's nice to have a debug build by default...

As is explained in the RFC and as I elaborated in the comments, this RFC does not intend to change the culture of which build type you use during development. That build type will remain debug, it's just that now you'll have to pass cargo build --debug instead of just cargo build. This is to prevent people who are just calling cargo build from getting an unintentionally slow build. With shell command history, the "extra typing" is a non-issue.

nmatsakis: One comment is that it would surprise me if optimizations were on by default.

The RFC lists lots and lots of examples (although I could always come up with more; tons are around) of people being surprised by debug-by-default. If someone were surprised by opt-by-default, it would only be so because of poor design decisions made in older compilers that have created this unfortunate convention.

Even so, let's look at the consequences of the "surprising default" in both cases:

debug-by-default: if you wanted an optimized build instead, your production binary is now incredibly slow. As someone who has actually pushed unoptimized binaries to production by accident, I can confidently say this costs a lot of money.

opt-by-deafult: if you actually wanted a debug build, the failure state is "you waited longer for your build to finish than you should have (and no debug symbols)," which while annoying, has far less potential for damage.

nmatsakis: [...] Also, has anybody looked at the combinations of settings to see if there's a better intermediate setting?

Having a "half-optimized" build for default that differs from release mode would only further confusion, because now there are three build modes: debug, release and a weird default mode. The whole point of this RFC is to minimize the chances of people running non-release builds by accident. With a weird third state, that new state (while better than debug) would also show up in production environments.

nrc: I feel debug by default for rustc is great, since that's what C compilers do.

Doing something only because "that's how C compilers do it" is a very weak argument. Why promulgate hurtful design decisions made 40 years ago? Merely for the sake of consistency? Taking that argument to the absurd, Rust shouldn't have a borrow checker since "C compilers don't have it either."

pcwalton: [...] With compiled languages, pretty much all (except go) do not optimize by default.

Same as above; this isn't much of an argument.

pcwalton: [..] If we go to optimizing by default, we just move from one set of problems to the other. I suspect that if we change the default to optimized, people will stop complaining about benchmark performance but instead will complain about compile times.

It's correct that it would be trading one problem (accidental slow executables) with another (accidental slow builds), and that's the whole point since accidental slow builds are a vastly safer and preferable choice. The second is merely annoying while the first one leads to expensive mistakes.

In other words, this problem-trading is a feature, not a bug.

To draw a parallel with other Rust design decisions, the borrow-checker is trading considerable up-front programmer effort to save even more effort in debugging memory leaks down the line. One could say "but now everyone has to wrestle with this new problem of convincing the compiler they've written memory-safe code; we've merely traded one problem (possibly memory-unsafe code) with another (harder to write the code in the first place)."

Safe design rarely comes for free, but it pays off massively by eliminating lots of future mistakes.

steve: I tweeted about this. Most people said they expect to have to opt out of optimizations from their build system, not opt into them.

Thanks for this! Super-happy to see it, although I can't say I'm surprised by the result.

@tbu-
Copy link
Contributor

tbu- commented Mar 11, 2015

With shell command history, the "extra typing" is a non-issue.

Extra typing is always an issue.

@tbu-
Copy link
Contributor

tbu- commented Mar 11, 2015

Merely for the sake of consistency? Taking that argument to the absurd, Rust shouldn't have a borrow checker since "C compilers don't have it either."

I don't want that the discussions ends up using too many rethoric devices, please don't do that.

I think it's evident that Rust strives for C-like behavior in (at least some) cases where it's not a safety issue.

@tbu-
Copy link
Contributor

tbu- commented Mar 11, 2015

Same as above; this isn't much of an argument.

Telling people that what they say is not an argument does not help the discussion.

As for this specific point, consistency with other languages is a plus. This is an argument and I consider trying to defeat it with "this isn't much of an argument" unacceptable in a useful discussion.


# Alternatives

## Only have `cargo` echo a message stating `debug` or `release` build mode
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this alternative. If someone doesn't know what optimized/unoptimized means, it should be relatively easy to google "rust optimized build". I don't think the argument of "A Rust newcomer who's only used Python, Ruby, JavaScript, Java, C# or a similar language is not going to understand the consequences of a "building in debug mode" message." is very convincing. Rust targets cases where one would reach for C/C++/maybe Java, not any of the listed languages. In all those languages, debug is the default.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice people have come to Rust from a large variety of languages, so we can't assume that they have C/C++ habits already.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They don't need any uniquely C/C++ habits. All they need to do is read cargo output to realize the build is unoptimized.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rust targets cases where one would reach for C/C++/maybe Java, not any of the listed languages.

That doesn't mean that only people using those languages will be looking at Rust. @steveklabnik was a Rubyist (if I'm not mistaken) before coming to Rust. Plenty of other examples abound.

In all those languages, debug is the default.

And in all those languages, it's a very unsafe default (as this RFC explains why). The idea is to change that in Rust and break away from the harmful, legacy behavior, much like the borrow-checker lets us break away from the errors of manual memory management in C & C++, while still leaving that feature available.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not true that this is "harmful legacy" behavior, it's behavior actually expected by some members of the community (faster builds). You present it like there's only one choice, namely defaulting to release builds, whereas a warning might suffice.

I think that this is absolutely not on the scale of memory-unsafeties, because people want rightfully something else most of the time. Comparing it to borrow-checking is not useful.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not something that I would consider "unsafe". "Unexpected by some" would be my preferred phrasing. Also, just because people come from other languages doesn't mean that rust needs to cater to their expectations.

I, for one, definitely expect to get a debug build with the "minimal-typing way to build my stuff" command. If it does otherwise, I'll be a little bit surprised, but as long as the build system properly communicates what it's doing, that's fine. Right now, we don't communicate the build type at all. If that's fixed, all should be right with the world.

Points that I do agree with:

  • Put debug binaries in target/debug/ instead of target/. Clear the "other" directory on build unless both debug and release flags are passed.
  • Embed the word "unoptimized" or "unoptimized" in the build output.
  • Make rustc optimize by default (as opposed to cargo).

@mahkoh
Copy link
Contributor

mahkoh commented Mar 12, 2015

@Valloric I see you've missed my suggestion to make this configurable via configuration files and environment variables. You might want to add this to the RFC or explain why you disagree with it.

@Valloric
Copy link
Author

@alexcrichton Thank you for your detailed comments! Sorry for the delay in responding to them, I've been super-busy.

The first point of the motivation I feel will be largely mitigated with two changes to cargo:

  1. Place output in target/debug by default (already implemented)
  2. Mention that cargo build is a debug build in the output (soon to be implemented)

While I greatly appreciate these changes (they're awesome, thank you!), I'm not convinced they entirely address the problem. The main reason is that "debug" does not imply "slow" to people coming from dynamic languages or Java/C#. For those of us used to systems languages, the implication is common. For something like Python, it's an utterly foreign term that implies better debuggability but not slower performance.

Case in point, for C# Visual Studio provides a Debug and Release mode but the perf difference between the two is utterly minimal. The difference mostly comes down to the availability of debug symbols. From the linked source:

Debug vs Release setting in the IDE when you compile your code in Visual Studio makes almost no difference to performance… the generated code is almost the same.

For Java, the concepts "Release mode" and "Debug mode" might be familiar to the user, depending on which IDE they use. The compiler options that javac provides all affect the availability of debug symbols, just like for C#. From the linked source:

The jar files will be a little smaller, but the performance benefit is minimal (if any).

For Python, passing -OO to the python executable to increase optimization results in code that's smaller (no docstrings) and doesn't have assert statements. So again, debug mode is effectively equally fast but more debuggable.

So of the Big Five languages that do have some concept of Debug and Release mode, the Debug concept is very much linked to "increased debuggability with almost no impact to performance." For others, the concept doesn't exist and thus has no linkage to lower performance.

So I remain unconvinced that seeing the string "debug" in cargo output or even the binary location will deter people unfamiliar with systems languages.

You've brought up the point that this sort of output is easy to gloss over and forget, but I'm specifically thinking in the case of someone new to the language they probably won't be so quick to gloss over the output and will probably pick it up

With the "easy to gloss over the output when included with lots of other text" argument I'm mostly referring those already familiar with Rust. I've linked to examples of Rust veterans shooting themselves in the foot with debug-by-default; other experienced users of Rust have mentioned similar experience in this thread, on r/rust and IRC.

For the second point of motivation I largely agree with @huonw in that it's a matter of proportion. Sure it's a problem that can happen, but it doesn't seem any more so than other footguns such as using unsafe when you probably shouldn't be.

I think you misunderstood @huonw's point in the comment you linked to; he was saying that bigger companies are in fact probably more likely to absorb their correspondingly-higher losses from mistakes than smaller companies. In other words, while the size of the mistake is a matter of proportion, a "merely" $10,000 mistake can sink smaller players.

I agree that unsafe can easily be a footgun, but the way we recommend its use actually supports the argument for opt-by-default. We don't allow unsafe code by default, and assume that people should know when to put code in safe blocks; it's the other way around because safe code should be the default. In much the same way, an optimized build that one can run & deploy without costly mistakes should be the default.

The defaults to me represent what you use the majority of the time, hence my comment.

I won't pretend that there isn't a drawback in opt-by-default since we do expect experienced devs to use debug builds far more often than release builds. But I believe this extra cost of passing --debug (which is minimal with modern shells) is far outweighed by a design that's safe by default. As I have pointed out in the RFC and in comments here, the failure-case for wanting a debug build but getting a release one is merely annoying, while the opposite case can be (and has been, in similar tools) catastrophic.

In a sense, I see this to be similar to the borrow checker. As someone who has been writing C++ code for years, I very rarely actually have a memory leak in my code since I track memory ownership well enough. So the borrow checker would be a nuisance 99% of the time. And yet I welcome having it turned on 100% of the time in Rust (along with the costs that come with that) just so that I can avoid that very painful 1%.

That's what it boils down to, IMO. Yes, some cost has to be paid in the 99% case to ensure that the catastrophic 1% case is far less likely to happen. I see that in the borrow checker and in opt-by-default.

All examples you've listed can be summarized by someone new to Rust who was unaware of the optimizations the compiler has.

That's not true for all of my examples (please look again). You can also see @gankro echoing similar sentiments of getting bitten by debug-by-default, and no one would call him a Rust newbie. :)

For example we have quite a few examples of where the borrow checker surprises newcomers, but we're not proposing to turn it off by default!

I don't think this is a fair analogy; the borrow checker decreases the likelihood of mistakes, and so would opt-by-default.

I also see that the point has been made many times in this thread about how "debug by default" is a poor design decision, but I'm not sure I've seen a clear statement as to why. To clarify, are there reasons beyond a performance footgun that you think this is a mistaken design decision?

Other than the absolutely massive general-safety issue of the performance footgun, no, I don't see any other issues. In the same vein, I don't see any point in the borrow checker beyond it "merely" ensuring I don't have memory safety issues.

In other words, they both guard against a huge enough problem. (Note that I'm not saying they are equally-sized problems, just huge enough to be worth guarding against by default.)

I find conventions can be quite a strong argument, and disregarding the years of experience and learning other compilers as done should not be done lightly.

I agree 100%. I am sure that the decision to include a paradigm-shifting borrow checker in Rust was not made lightly given that C and C++ have no such component and have been memory-unsafe for decades with billions of lines of code written in them.

Can you please elaborate a little more on why you think C compilers are mistaken to not optimize by default? I'd like to drill down specifically into why you don't place much weight in this historical argument.

The performance footgun of getting a debug build when one wanted a release build. It's far too easy to encounter this; the RFC goes into detail.

As is listed in the RFC, I do not believe that the creators of C compilers made a mistake; they were bound by backwards compatibility. Initial compilers didn't have optimizations at all, so adding options that change the generated code had to be done without disrupting how people were already relying on the compilers to behave.

I agree with @reem here in that we should not be designing our tooling with the assumption that extra tooling is in place. Lots of thought has gone into making both rustc and cargo as ergonomic in their own right.

I hugely appreciate the tons of work that have gone into rustc and cargo to make them as ergonomic as possible; I thank you personally for this since I know you've put lots of effort into cargo (and, of course, rustc).

But I also think that assuming that the tools will be used in a vacuum is a mistake, since they won't be.

I suspect that if rustc with optimizations on was as fast as with optimizations off there would be much more widespread agreement here, but unfortunately that's not the situation we're dealing with.

Truer words have probably not been spoken in this thread. :)

Do you feel that we should go with a substantially more error-prone design (which we'd have to live with forever) because today, rustc is pretty slow at producing optimized code? That would strike me as a very short-sighted decision.

Finally, thank you for putting in the time to review this RFC, read my responses and provide a lengthly list of comments! While I may disagree with some of them, I still value them all immensely.

@Byron
Copy link
Member

Byron commented Apr 1, 2015

After reading this question on stackoverflow, I felt the need to finally share my feelings on this matter. It's not my intend to pick up every argument given here so far, but to write about my intuition, about what I would expect when using cargo. Maybe it adds some value.

In the following, I will list some common cargo operations, along with the default settings I would expect. All statements marked with (OK) are already present in the cargo we know today.

  1. cargo test
    • full debug mode without optimizations (OK)
    • dependencies are compiled in --release mode
  2. cargo bench
    • same as if compiled with --release (OK)
    • dependencies are compiled in --release mode (OK)
  3. cargo run|build
    • same as if compiled with --release
    • dependencies are compiled in --release mode

As usual, all default settings should be overridable per target in the Cargo.toml file. My thinking is that defaults are the right thing™ for the majority of the cases, which implies one must allow overrides for all other cases.

About explicitly stating the compilation mode of dependencies

To me it seems that the majority of time, one will pull in third-party libraries. Therefore one is not interested in debugging them, but in their optimal performance. For example, if I am writing a command line tool to process data using a third party library, the latter should always perform well even though I am debugging my own application.

Conclusion

Cargo already behaves in accordance to my intuition in many cases. The main issue I see is cargo run|build not being --release by default. Higher compile times can't be a reason to argue against having intuitive defaults - after all, performance of both rustc as well as cargo are likely to improve over time. I can't judge rustc, but in case of cargo I see a lot of potential to increase performance just by not doing so much unnecessary work.

A major issue that seems to remain, but might deserve (a possibly separate) discussion, is how to handle the compilation mode dependencies. Release by default is what I would expect in this case.

Off-Topic, but related

cargo test and examples

Cargo supports to build each .rs file as command-line program if dropped into the examples folder. This happens automatically when running cargo test, which implies debug settings for everything. Even though I understand why cargo test will build these programs, to me it makes no sense to not build them in --release mode, as the intended audience is not the library's author, but users of that library. They will be interested in seeing optimal performance when running these programs.

Tying examples to cargo test seems to be a problem in this case, as those who are inclined are unable to build these in --release mode unless they write their own Cargo.toml to allow cargo run --release. capn-proto may serve as example for this issue - also note that cargo test will not pick this one up as example anymore, which is not in the authors interest. Currently, the author has to choose between his own and the audience's interest, which simply shouldn't be.

Possible cargo performance improvements

The main issue I see with cargo is that it currently duplicates a lot of work unnecessarily. For example, many dependencies are actually shared among multiple projects you are building. Even though it makes sense to, by default, store build products by hash from all relevant factors within the project folder, it would be great to have an option to specify an alternate folder for this kind of output. That way, one could define an alternate folder per user in .cargo/config or per multiple projects and share build products among them.

Performance wise, this is only relevant for initial builds, yet they happen often enough during my particular workflow to feed the desire for the said improvement. As a side-effect, there will be no unnecessary consumption of disk space.

As an extreme example, have a look at this output for my current project:

➜  google-apis-rs git:(master) l
total 64
-rw-r--r--   1 byron  staff   2.6K Feb 27 09:52 CONTRIBUTING.md
-rw-------   1 byron  staff   6.9K Apr  1 10:27 Cargo.lock
-rw-r--r--   1 byron  staff   644B Mar 20 14:50 Cargo.toml
-rw-r--r--   1 byron  staff   1.2K Mar 13 15:01 LICENSE.md
-rw-r--r--   1 byron  staff   3.1K Mar 24 17:07 Makefile
-rw-r--r--   1 byron  staff   5.4K Mar 25 14:28 README.md
drwxr-xr-x   5 byron  staff   170B Mar  1 16:29 etc/
drwxr-xr-x  78 byron  staff   2.6K Mar 24 17:43 gen/
drwx------   8 byron  staff   272B Mar 18 07:51 src/
drwxr-xr-x   3 byron  staff   102B Mar 20 11:04 target/
➜  google-apis-rs git:(master) l gen
total 0
drwxr-xr-x    9 byron  staff   306B Mar 25 14:28 adexchangebuyer1d3/
[... 75 entries hidden ...]
drwxr-xr-x    9 byron  staff   306B Mar 25 14:28 youtubeanalytics1/
➜  google-apis-rs git:(master) du -sch gen
6.9G    gen
6.9G    total

The projects in gen are generated, and share all dependencies. Yet each one must currently have an own copy of the very same dependencies.

@Nemikolh
Copy link

Nemikolh commented Apr 1, 2015

Please do not make build optimized by default, it make no sense.

I would really like to see a vote on that topic by people who use on a day to day basis a system programming language.

If really some people out there manually push debug build to production, then It would be a real pleasure for me to teach them about Build Automation. This is a really simple process to put in place. It is easy to do and can be done in roughly 1 hour. And if you need a more complex system, you can use your google skills to find the build bot of your dreams. It'll save you a lot more money than discussing about default for rustc and cargo.

@iopq
Copy link
Contributor

iopq commented Apr 2, 2015

Why does it make no sense? Most languages like Java and C# optimize by default. There is an audience overlap between Java/C# and Rust because they are all languages that you could write a high-performance server in.

In fact, Java servers are so optimized they sometimes beat pure C implementations in benchmarks. If I were a Java programmer evaluating Rust I would never find out about compiling with optimizations on because it's just not prominently mentioned. This happens all the time.

Don't just say it makes no sense without substantiating it.

@Nemikolh
Copy link

Nemikolh commented Apr 2, 2015

Are you realizing that you're comparing JIT languages against Rust, an LLVM based language ? That definitely make no sense.

As a reminder, the rust-lang.org page says clearly that Rust is a systems programming language. Thus, the main class of languages to compare Rust with, is one such as C or C++.

Besides, if you want to talk about Java, all the optimization phase is done by the JIT compiler at runtime. There's no such thing as a Java optimized byte-code build with the javac compiler.
Debugging a java application is almost free in time, you just have to restart your application with different arguments passed to java. Whereas in Rust, C or C++, it requires a special build of the application that takes time.
Debugging is quite common when you develop, in fact you really don't care about optimization until you start benchmarking some code or you want to push in production a version of your application. But for most people, that's not where they're spending most of their time.

I would never find out about compiling with optimizations on because it's just not prominently mentioned. This happens all the time.

So far, newbies would just look for "why my rust application is slow" and find the first answer on stackoverflow solving their problem. That's probably how you find out about it. And this is just a natural step needed when learning about a systems programming language.

I do agree though that a hint explicitly saying that the build is in debug mode from cargo would really be helpful for that case.

Finally, coming back to debugging, you can use without trouble rust-gdb against a release build with symbols included. Then, the newbie question that we would see everywhere would be:

"Is rust-gdb broken ? It looks like my code randomly jumps from one place to another. Some statements are completely ignored. Is it normal ?"

It's just trading one problem with another. In both cases, a newbie will have to learn what kind of language Rust is. IMO, by default, you want the best option for the day to day developer.

@iopq
Copy link
Contributor

iopq commented Apr 2, 2015

Java does do optimizations at compile time, not just at runtime.

@ghost
Copy link

ghost commented Apr 6, 2015

This has tripped me up as well.

From my experience, it doesn't matter which is default - one default is just as confusing as the other, depending on what I'm expecting, and it doesn't matter what I'm expecting. It just matters that I know what's happening. With the current setup, the easiest way to find that out the default compilation settings are via IRC or google. And I think we can agree that's not ideal.

This is what I'd appreciate:

  1. Have a flag in the Cargo.toml for whether to verbosely output the current profile's settings. For example, verbose-profile = true outputs Compiling with debug: true, debug-assertions: true, opt-level: 0, link-time-optimizations: false

  2. Have cargo automatically add an explicit dev profile section to new projects' Cargo.tomls, with the default compile settings and the above flag.

# The development profile, used for 'cargo build'
[profile.dev]
opt-level = 0  # Controls the --opt-level the compiler builds with
debug = true   # Controls whether the compiler passes -g or '--cfg ndebug'
debug-assertions = true  # Controls whether debug assertions are enabled
verbose-profile = true # Controls whether these profile settings are output while building
  1. For rustc, I doubt anybody expects this output by default - terminal applications tend to be mute until something goes wrong. Either a similar --verbose-profile flag could be added(ick), or it could be piggybacked onto the existing --verbose. The latter seems ideal, as it's well known and quite broad in what it promises ("Use verbose output.")

This way, there is absolutely no confusion as to what the output binary is going to be, and the defaults will be unchanged. The crusty old-timers will be comfortable, while the young-and-restless know what they're getting themselves into. :)

@mkpankov
Copy link

I'm against this. It slows down compilation and produces less debuggable programs by default.

The best option I can think of is adding to cargo build output something like Optimization is off or Optimization is at level O1 etc. to let users know what's happening.

@Valloric
Copy link
Author

@alexcrichton Ping? Rust is approaching 1.0 and if we're willing to implement this RFC, doing it before 1.0 would be a good idea. It might also be acceptable to do it post-1.0 if we're willing to break rustc/cargo command-line backwards compatibility (which wouldn't be affecting the Rust language or the libs). Still, doing it before 1.0 would be a much better idea.

@iopq
Copy link
Contributor

iopq commented Apr 22, 2015

@mkpankov I want an optimized build 99% of the time because when I test my game-playing AI it makes no sense for me to test it at 1/10 speed - I would have to give it 10x the time for testing which would actually slow me down.

So it definitely depends on how useful unoptimized builds are for you. I just cargo build and Ctrl-C before it generates a binary because I just care about it type checking.

@mkpankov
Copy link

I just cargo build and Ctrl-C before it generates a binary because I just care about it type checking.

Your desires are... unconventional

@mkpankov I want an optimized build 99% of the time because when I test my game-playing AI it makes no sense for me to test it at 1/10 speed - I would have to give it 10x the time for testing which would actually slow me down.

Very well. That's just one use case out of myriad. Testing and debugging are different goals.

I'm with @Kingsquee in this case - profiles are generic enough solution and everybody can just use profiles they need.

@alexcrichton
Copy link
Member

@Valloric

Gah I'm so sorry about taking so long to get around to this, it's been sitting in my inbox for ages!

So after re-reading your last comment (which thanks for taking the time to write!) I think the main difference in our opinions is the degree at which debug-by-default is harmful. It sounds like you think it's quite a serious issue for a number of reasons, but I personally remain much farther on the other side of the spectrum in the sense that I would primarily see this as a nuisance than a critical problem.

At this point I think I'm a little less persuaded to keep the status quo because of compile times. @Kingsquee had a good point where it's likely for both defaults to be just as confusing as another. Put another way, I have to remember to make my binary fast right now, but if we switched the defaults I'd have to remember to make my build fast.

I do think, however, that it's still a pretty important point that we've had unoptimized-by-default for so long that we've started designing with that assumption by turning off debug assertions, overflow checks, etc, when optimizing. Additionally, we have small features like the -O flag to the compiler which would need to be replaced.

Overall, I feel that there's compelling arguments on both sides of the fence here. I am not as persuaded as you are that one outweighs the other, which makes me lean more towards maintaining the status quo.

I do agree that that this sort of change would need to be considered before 1.0, and unfortunately that deadline is rapidly approaching. If consensus is reached in the next week or two to switch the defaults here then we can probably still make the change, but it does not seem likely that consensus will be reached.

@Valloric
Copy link
Author

@alexcrichton Thank you for your thoughtful-as-usual comments!

It sounds like you think it's quite a serious issue for a number of reasons, but I personally remain much farther on the other side of the spectrum in the sense that I would primarily see this as a nuisance than a critical problem.

Fair point, reasonable people can disagree. :)

[...] it's likely for both defaults to be just as confusing as another. Put another way, I have to remember to make my binary fast right now, but if we switched the defaults I'd have to remember to make my build fast.

Sure, I can see how both defaults could be confusing, depending on the user. But IMO the question is which of the states (opt or debug by default) has the potential to lead to more damage. In other words, while both can be confusing, opt-by-default is the safer choice and should thus be preferred.

I do think, however, that it's still a pretty important point that we've had unoptimized-by-default for so long that we've started designing with that assumption by turning off debug assertions, overflow checks, etc, when optimizing. Additionally, we have small features like the -O flag to the compiler which would need to be replaced.

I don't see why we'd have to replace any of the flags; I see it more as the user getting a certain selection of flags passed by default if they don't pass any.

I've also mentioned above that developers will probably want to use cargo build --debug during development (and should be encouraged to do so by the community/docs), so all the lovely things we get as part of debug builds will remain available.

In other words, the changing of the default state is targeting newbies and forgetful experienced devs. If you've sat down and are actively iterating on the code, you're using cargo build --debug. That's the idea.

Overall, I feel that there's compelling arguments on both sides of the fence here. I am not as persuaded as you are that one outweighs the other, which makes me lean more towards maintaining the status quo.

If we really go with debug-by-default, it should be because reasonable people have weighed the arguments and have decided that is the more sensible outcome, not because of inertia. We shouldn't be taking the easy choice of inaction just because it's available.

If consensus is reached in the next week or two to switch the defaults here then we can probably still make the change, but it does not seem likely that consensus will be reached.

Consensus is great when it's available, but when it isn't, a tough decision needs to be made. I know @pcwalton is fond of saying that the core team needs to be able to make decisions it finds to be in the best interest of the project even if consensus can't be reached (rightly so, IMO).

"A consensus couldn't be reached" shouldn't result in "we've decided to make no decisions." Decisions everyone agrees on aren't the hard ones; they require no leadership.

Lastly, thank you for your time here again; I know you guys are super-busy getting 1.0 out the door.

@Diggsey
Copy link
Contributor

Diggsey commented Apr 25, 2015

Sure, I can see how both defaults could be confusing, depending on the user. But IMO the question is which of the states (opt or debug by default) has the potential to lead to more damage. In other words, while both can be confusing, opt-by-default is the safer choice and should thus be preferred.

I completely disagree: debug by default is the safer case, because it is more likely to catch errors in the program. With debug by default, the worst case is that your program runs slightly slower than it could do. With optimise by default, the worst case is that there are a whole host of overflow and other problems with the code that will cause the program to malfunction at some unknown point in the future.

In addition, the vast majority of compilations are when debugging and iterating on code, whereas producing a release build is a relatively rare event.

There's also plenty of scope for eg. IDEs and other tools to improve the situation by automatically passing the correct flags. In those situations it doesn't really matter what the default is.

@iopq
Copy link
Contributor

iopq commented Apr 26, 2015

Vast majority of compilations are either cargo build followed by a Ctrl+C because I don't want an executable, or cargo test which would be debug always.

I cargo build to see if the code type checks, I never really use the executable from cargo build even if I let it run to completion. If I need to see how it works and run it, I always use cargo build --release because the performance of cargo build binary is an order of magnitude slower and thus useless.

If your project doesn't really have any difference between optimized and unoptimized builds, that's great. But most serious Rust projects use it because of its performance characteristics, so trying to profile or debug an unoptimized program will be useless because it's so much slower than the optimized one.

I don't feel strongly about which should be the default, but I think that there should be some discussion of what kind of build people choose to do and what the expected outcome of that kind of build should be. For me it's something like:

  1. cargo build - after I write new code, does it compile? I don't care about the output. I wish there was just a cargo build --dry or something that doesn't even do the LLVM pass.
  2. cargo test - after I write tests, I do care about the output executable, but the tests usually run fast
  3. cargo bench - after tests pass and I want to see if anything got slower. Tests run fast so they can be debug and slow.
  4. cargo build --release - to "integration test" the latest changes to make sure it performs as well as the previous version. This is also the version I ship.
  5. A custom profiler build that has debug info and limited optimizations to feed into a profiler to see where my hotspots are. I also want to conditionally disable inlining to give more info to the profile.

So maybe I want is not just a certain set of defaults, but the ability to customize the build process with cargo to what I want it to be for that project.

@nagisa
Copy link
Member

nagisa commented Apr 26, 2015

Vast majority of compilations are either cargo build followed by a Ctrl+C because I don't want an executable

rustc already supports this with either -Z no-trans or -Z parse-only. You should nag on cargo issue tracker to add functionality which represents these flags.


As for me, I’m strongly in favour of debug by default. We turned off LLVM and rust assertions in beta build already and now people started closing rustc/LLVM ICEs because they don’t assert anymore. That’s rust contributors, of all people!

I strongly suspect the similar thing would happen periodically with rust programs if we go with optimised-by-default:

  • “Why doesn’t this throw overflow panic even though rust has overflow checking by default?!”;
  • “Why does this mutex not panic, even though it failed to lock due to [some obscure reason like use after free via unsafe code]?! Took me 10 hours to debug this! Rust sucks!”;
  • etc.

For comparison one of the largest rustc sub-libraries – libsyntax – time to compile at every optimisation level (with a stage0 compiler, which is compiled with optimisations):

  1. 1:30.30
  2. 1:52.70
  3. 2:26.50
  4. 2:32.21

Compiling libsyntax with stage1 (non-optimised): … more than 22 minutes (killed before completed)

Even then, I’m strongly in favour of debug builds with as much checking as can be done by default. The only thing this proposal fixes is: “huh, my program is slower than expected, strange”.

@alexcrichton
Copy link
Member

@Valloric

I don't see why we'd have to replace any of the flags; I see it more as the user getting a certain selection of flags passed by default if they don't pass any.

Ah yeah this is a good point. If we were to switch the defaults, we would ideally remove the -O flag from the compiler and --release from Cargo (in favor of something like -D and --debug), but we can always leave the old flags around for compatibility of course!

If we really go with debug-by-default, it should be because reasonable people have weighed the arguments and have decided that is the more sensible outcome, not because of inertia. We shouldn't be taking the easy choice of inaction just because it's available.

I completely agree with this. There have been many arguments on this thread in both directions, however, so I think it's quite safe to say that deciding on debug-by-default would not be only because of inertia.

Consensus is great when it's available, but when it isn't, a tough decision needs to be made.

I also agree with this, but this is seen as a fairly radical change by many, in which case broader consensus is generally expected before moving forward. For example when renaming int and uint there was quite a lot of discussion in many directions, but the magnitude of the change required much broader consensus for moving forward (which was eventually reached).

@codyps
Copy link

codyps commented Apr 27, 2015

I suppose another thing to consider is having something like gcc's -Og: enable some optimizations, but avoid those that significantly degrade the debugging experience. In rust's case, that could mean doing things to avoid overly-long compile times.

As for defaults: all of my "to production" deployment happens via packaging scripts which already know that they need to pass --production to cargo, so for me I'd much rather have the default case optimized for debug-ability.

I think it's important to consider here what we expect the "manual" usage of cargo to be. And personally, I don't expect production deployments to be be the majority of manual cargo invocations.

@codyps
Copy link

codyps commented Apr 27, 2015

Another thought: for cargo, we could allow configuration of the default (op-level, debug-assertions, etc) via $HOME/.cargo/config. That way everyone could add their own preferred default configuration.

@Byron
Copy link
Member

Byron commented Apr 27, 2015

@jmesmon A great idea ! Cascading configuration is very practical, after all, and might help to end this debate with everyone being happy.

@Valloric
Copy link
Author

I could live with opt-by-default while having a ~/.cargo/config script where one could override the default (with the command-line flags overriding the implicit config of course). I think this is still somewhat error-prone but would nonetheless be a massive improvement over the status quo. With this, IMO no one could complain about opt-by-default slowing down their iteration or whatever because they can just change the default for themselves.

I honestly think this covers all the complaints about opt-by-default while providing the safety & usability win for newcomers.

@iopq
Copy link
Contributor

iopq commented Apr 28, 2015

How about optimized builds with overflow checks and assertions on? For my use case I would be interested in something like that.

@alexcrichton
Copy link
Member

@jmesmon the idea of a .cargo/config (or some other means of global configuration) has also been discussed previously, but it's certainly an alternative! It's unclear whether it's a silver bullet for this situation though.

@iopq any manner of configuration of the compiler is certainly possible (via flags, etc), I think that this is largely just a question of the defaults. It is certainly true, however, that the default could be debug assertions + optimizations turned on. (not sure if this would be desirable, though)

@Gankra
Copy link
Contributor

Gankra commented Apr 28, 2015

Overflow checks are a very interesting case. My understanding is that in numerics-heavy code they can have a huge overhead (preventing vectorization?). So it seems contradictory to enable optimizations by default but then leave in overflow checks. Similarly we've been defensive-debug-assert-happy in critical paths on the assumption that an optimized build would strip these. This could have have the odd side-effect of people believing that the default build is totally optimized, when really it's leaving important optimizations on the table.

On the other hand, having overflow checks off by default could lead to people writing code that doesn't work at all in debug mode because it instantly starts triggering tons of overflows because they didn't use wrapping ops and never had a reason to enter debug mode. Not good!

@lilyball
Copy link
Contributor

But most serious Rust projects use it because of its performance characteristics, so trying to profile or debug an unoptimized program will be useless because it's so much slower than the optimized one.

I think this is a really bad generalization to make. In CPU-bound programs, sure, performance is generally very important (though not always to the degree that you're suggesting). But not all programs are CPU-bound programs.

As an anecdote, my day job is writing iOS software, and the current project is a (relatively) large Swift-only project. Swift has followed Rust's model of relying on the optimizer to get orders of magnitude performance differences in CPU-bound code. And yet that rarely makes a difference in my program, because it's an application that is usually bound by networking or by user interaction, not by number-crunching. The compile-time difference between optimized and unoptimized builds is huge, but the observable runtime performance is pretty minimal.

Which is to say, everybody's requirements are different, and some people are writing programs that are pretty useless without optimization but other people are writing programs that work just fine without optimization.

I could live with opt-by-default while having a ~/.cargo/config script where one could override the default

I think this is dangerous. There have been multiple claims that debug-by-default is "dangerous" because people will forget to pass the --release flag before building and installing a binary. I find that claim to be fairly ridiculous, because the likelihood that the install instructions for a binary is "run cargo build" is pretty darn low (given that cargo does not handle installation of binaries), and any distribution process is likely to involve a build script that invokes cargo build as one of its steps. Same goes for any non-executable software, anything that's actually used in production is going to be handled by some kind of build script, just like anything installed on an end-user's system. So the defaults of cargo build are basically only going to affect developers.

That said, a ~/.cargo/config script that changes the default build profile is dangerous. It's dangerous because developers who set it to build release by default are likely to end up forgetting that they've changed the defaults over time and may end up writing a build script that invokes cargo build instead of cargo build --release. It will work just fine for them, and for anyone else that's changed the default build profile, but anyone running a non-customized cargo install will end up with an unoptimized binary.


Ultimately, I'm still very much in favor of the status quo. Optimize-by-default is risky because it leads to not running debug assertions or overflow checks during development, it breaks convention with every other compiled language I'm aware of (and therefore defies user expectation), and it has just as much risk of leading to public perception of "rust compiles very slowly" as debug-by-default does of "rust programs run slowly".

@lilianmoraru
Copy link

This is a very bad idea, I don't understand why it's still discussed.
The most usage of cargo build you see is during the development cycle.
The default in this case should always be the option that helps us write better code. A release version is usually made in just the smallest fraction of times compared to a development build.
I can't imagine of a case where somebody uses a technology seriously and they don't have any idea about it(In this case that there is a --release build).
We should not optimize for the 1%(the people that try a Hello World style application) instead of the 99%(the people that will use it daily for development).

@alexcrichton
Copy link
Member

Ok, at this point it looks like this RFC has basically run its course, and I think that one of the key points I've seen is that despite which default is chosen, there will likely be pitfalls either way. There's been quite a bit of discussion about the severity of these pitfalls and how it should possibly play out in terms of choosing a default, but there is not clear consensus on changing the defaults.

As a result, I'm going to close this RFC and we're likely going to be sticking with the status quo. I'd like to emphasize though that closing this RFC is not sticking with the status quo "because it's what we did before", but rather that there are legitimate arguments for debug-by-default (spelled out in this thread) and there is not enough consensus for making such a change (and many a very much not in favor).

Regardless though thank you for the discussion everyone, and especially @Valloric for keeping up with this RFC and taking the time to write it out!

@tshepang
Copy link
Member

tshepang commented May 6, 2015

great decision there @alexcrichton

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.