43 ms·
Fast Rust Builds
- nextaccountic 5y ago> If you do need generic interface for better type-safety and ergonomics, make sure that the interface layer is thin, and that it immediately delegates to a non-generic implementation. The classical example to internalize here are various functions from str::fs module which operate on paths: pub fn read<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> { fn inner(path: &Path) -> io::Result<Vec<u8>> { let mut file = File::open(path)?; let mut bytes = Vec::new(); file.read_to_end(&mut bytes)?; Ok(bytes) } inner(path.as_ref()) } -- Nobody seems to care, but there's a crate that does this for you https://github.com/llogiq/momo https://github.com/llogiq/momo But the Rust compiler should be doing this automatically :(
- epage 5y agoThere is interest in making this automatic, its just not been the highest priority for people.
- mrec 5y agohttps://github.com/rust-lang/rust/issues/77960 https://github.com/rust-lang/rust/issues/77960 I think.
- jstx1 5y agoAs a side note, looking at this block of code, the syntax is just so ugly and overcrowded. I don't get how a relatively new language ended up looking so messy so fast; it's like no one cared about readability.
- capableweb 5y agoIt is what usually happens when you design by committee instead of everyone having a common understanding and working towards the same goal under a single person/entity. Not saying it's good or bad, just how Rust ended up where it is.
- deleted 5y ago[deleted]
- pcwalton 5y agoWhat Rust committee?
- capableweb 5y ago"Design by committee" - https://en.wikipedia.org/wiki/Design_by_committee https://en.wikipedia.org/wiki/Design_by_committee I disagree it's a pejorative term as Wikipedia puts it though. Design by committee is in many cases necessary or even wanted (standard bodies).
- Zababa 5y agoI find the syntax very readable. What's your problem with it?
- kbenson 5y agoI imagine it's that there's a fairly high ratio if function signatures to other lines of code, and function signatures are particularly gnarly in rust compared to many other languages, at least when lifetimes are involved.
- Zababa 5y agoI agree that I find lifetimes a bit hard to read, but outside of that I usually find Rust's syntax really clear for an algol-like. For example, they use the ML-style type declaration variable: type, which I find more readable than the C-like type variable. There's a different syntax for namespace and function chaining, which I also like.
- infogulch 5y agoI wonder if this is a side-effect of rust function signatures being very precise, as in the function is very clear about the allowable type and ownership of its parameters and return values, where most languages can't even express things at that level of precision so people's eyes glaze over when reading it.
- kbenson 5y agoI imagine so. A function signature is rust encodes more information than a lot of other languages. From my (very) limited experience, without lifetimes they look fairly similar to C++, and then adding lifetimes just makes them busier and more complex, but that's because you're encoding additional information.
- jakobnissen 5y agoCompare to Julia # Your big function has specific types function my_function(x::UInt, y::UInt) [ ... ] end # Wrapper function to minimize monomorph. cost my_function(x::Integer, y::Integer) = my_function(UInt(x), UInt(y))
- scottlamb 5y agoPartly personal preference in both what is readable and in how much you optimize caller convenience vs simple callee code. I think it's fine to just expect the caller to do the conversion. In fact, it's often automatic. (Eg if you have a PathBuf, then just sticking a & in front of it is sufficient to supply &Path via Deref<Target = Path>.) So I'd write this function as follows: fn read(path: &Path) -> io::Result<Vec<u8>> { let mut file = File::open(path)?; let mut bytes = Vec::new(); file.read_to_end(&mut bytes)?; Ok(bytes) } which I find readable. Comparing to another language...I haven't written Go in a while, but I think the rough equivalent would be: func Read(path *Path) ([]byte, error) { file, err := os.Open(path) if err != nil { return nil, err } slice, err := io.ReadAll(file) if err != nil { return nil, err } return slice, nil } (Maybe in this case you'd just return the io.ReadAll rather than doing an if, but I'm trying to use similar constructs. The ? is something you can use all throughout the function, not just at the end, so I tried to match that pattern.) [edited: I said there wasn't a io.ReadToEnd extracted from io.ReadFile in Go, but jatone pointed out it's called io.ReadAll.]
- capableweb 5y agoI don't know Rust but converting that code snippet of Go code you wrote to (non-idiomatic [wouldn't use upper-case characters, namespaces used doesn't actually exists]) Clojure: (defn Read [path] (io/ReadToEnd (os/Open path))) Now that's what I call readable!
- brundolf 5y agoClojure is also a much higher-level language than Rust (and arguably Go too)
- scottlamb 5y agoYou can chain calls in Rust. If read_to_end were written to allocate and return a fresh Vec, you could do this: File::open(path)?.read_to_end() but instead read_to_end is written to take a presupplied Vec so the caller has the option to reduce allocations.
- nextaccountic 5y agoThe worst decision regarding Rust syntax was following C++ syntax. Due to this, we write, in type syntax Things<Like<This, That>, That> -- as in C++ Instead of a more legible (and very familiar to Rust authors) Things (Like This That) That -- as in Haskell Inherited from C++, we had also :: for namespacing (while a dot . could be perfectly be used for that), and tons of other stuff like <> for scoping generics. Without some of that we could have: pub fn read<P: AsRef Path>(path: P) -> io.Result (Vec u8) { fn inner(path: &Path) -> io.Result (Vec u8) { let mut file = File.open(path)?; let mut bytes = Vec.new(); file.read_to_end(&mut bytes)?; Ok(bytes) } inner(path.as_ref()) } Perhaps it looks a little better? It looks for me, at least. For doing better than that we could drop C-like syntax perhaps? The trouble is that languages have a weirdness budget - there's a finite amount of novel stuff a language can have until it becomes too weird for mainstream programmers. So language designers have to pick their weird features carefully. Spending it all in novel syntax is unwise. I think that at the time the Rust pull was safety for low level code and, as Rust was targeted to replace some C++ components, following C++ syntax was seen as a good idea.
- axelf4 5y agoSurely mixing usage of space and parentheses for function application is a bad idea?
- nextaccountic 5y agoBut I'm not talking about function application - I'm talking about type application. The syntax of type application and function application is already different in rust: F<A, B> vs f(a, b). Type application even has default and named parameters! F<X = A, Y = B> and F<A> if B has a default. I just think this particular type-level syntax is very noisy, specially because types in Rust tend to be very nested.
- Zababa 5y agoI like how the namespace and the function chaining have different characters. I think it makes it easier to read.
- 5y ago
- OJFord 5y agoTo take a guess at what you don't like, it can be equivalently spaced out a bit more like: pub fn read<P>(path: P) -> io::Result<Vec<u8>> where P: AsRef<Path>, { And of course if you were using a lot of u8 vec results, you might create a shorter alias for that.
- tanaypingalkar 5y agoI think rust syntax is more readable and self explains. Having syntax more minimal, more use of symbols, spaces and tab make them beautiful but they are really hard to get if it is written by someone else. I personally noticed it's more easy understand other rust project than other langs project.
- mamcx 5y agoThe syntax is ugly and can become overcrowded, but is in the HIGH side of readable. Compare: def read(path): What is path? Unknow. What it read? Unknow. What it return? Unknow pub fn read<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> { What is path? Anything that can become path on the file system. What it read? Anything that can be pointed with that PATH What it return? Vec of bytes or a IO::Error PLUS: This code is compiled tot he exact types that call it, Path not allocate, the result can be cloned, the errors are defined in IO, .... -- Rust is very readable: Almost any line tell you what is happening and what expect. Where it get messy is when it hide that crucial info. But is not many times it happens.
- void_mint 5y agoIf you're talking about a language being readable to those that aren't proficient, Rust is about as bad as one could imagine. Your example is also less about Rust and more about type signatures.
- mamcx 5y ago> Your example is also less about Rust and more about type signatures. No, is HOW Rust use type signatures. Rust is in the camp of "advanced usage", not like C#, Python and others, where a "type" is barely for classification. Instead, Rust use types to inform: Memory type (heap vs stack), Cost (cheap to copy or must clone and can be potentially very big), Permisions (read/mutable), Behavior, Share-ability (can go in threads), lazy or not (iterable), etc. It use types for a lot, and communicate much information densely. P.D: Not always perfect and I agree that the syntax is not as nice to me, but is incredible how much you can understand from most codebases just reading, even on docs (not need IDE assistance that much)
- void_mint 5y ago> No, is HOW Rust use type signatures. No, I don't really agree. The example was "def read(path)", which is intentionally vague and any typed language would clarify. Your points can be/are valid, but they don't really change the fact that the above example was a cherry pick, and Rust's (over)use of symbols and special characters make it, again, about as hard to read as one could imagine. Just because you _can_ derive meaning from it doesn't change that.
- pcwalton 5y agoCome on, it is definitely not the case that nobody cared about readability. My sleepless nights after arguments about syntax in 2011 and 2012 very much say otherwise.
- steveklabnik 5y agoOr the endless arguments about await, or that time I cried because the discussion over what we call integers was so contentious. We all cared quite a bit about syntax. Heck that’s partially why almost none of it is novel!
- matklad 5y agoI don’t think that when people say “Rust has ugly syntax” they mean that Rust has ugly syntax. It seems that what they are complaining about is actually Rust semantics, which needs extra syntax to express it. Blog post idea: express this snippet (with references, aliasing control, generics and all) in pseudo C++, Java, Python, etc. Then express “reading a file in gc language with exceptions” in pseudo Rust.
- scrubs 5y agoAgree.
- catern 5y agoOne sad aspect is the ? operator. In the beginning, Rust thought it could get away with explicitly handled return codes, without having support for direct style/exceptional error handling. But they've ended up with that in the end. If Rust had just used checked exceptions from the start (or equivalently if they had do-notation) they wouldn't have this lingering syntactic noise. Hindsight is 20/20 though - it would have been hard to convincingly predict that Rust would eventually end up with checked-exceptions-plus-syntactic-noise as its error handling paradigm - the experiment had to be carried out to see that it would end up like this. (Now, of course, exceptions have been thoroughly vindicated, and it would be silly to design a new language with explicitly handled return codes instead of checked exceptions)
- zozbot234 5y ago? is not "syntactic noise". Having an indication in the code of which operations might throw is very useful, and it enables a further simplification of using ordinary variant records as opposed to a special-cased "checked exception" construct.
- catern 5y agoCompare it to unsafe functions. An unsafe function can be called in an unsafe block or an unsafe function. Rust could require annotating each call to an unsafe function, even inside unsafe functions. Why don't we do that? That seems like it's also very useful for the exact same reasons. A proper language needs to allow the programmer to choose between implicit effects and explicit effects on a case-by-case basis. The reason ? is so tragic is because Rust tried to go with explicit effects only, but found that people wanted implicit effects, and ? is the closest that Rust can get. That's why it's syntactic noise: The programmer wants to get rid of it, they don't regard it as informative for this case, but they can't get rid of it. (And checked exceptions can be perfectly well implemented with regular data, you don't need any special cases - in fact, you can do it with the same infrastructure that's already been added in a special-cased form for async/await support...)
- GolDDranks 5y ago
- bschwindHN 5y agoI think the language team has done a great job. Rust may look a little ugly on the surface if you're just getting into it, but I'm super thankful they opted to require functions to always spell out all type signatures. Generics can get a little overwhelming but like any other skill, it just takes a few hours/days of practice to learn how to read them.
- runeks 5y agoDo you have any constructive suggestions? It looks pretty readable to me, and I’ve never written any Rust.
- fmakunbound 5y agoRegular jack-off here. It’s hard enough coding in Rust as-is. In addition to that, I don’t think I could take into consideration a coding style just to bring the build times down.
- zozbot234 5y ago> But the Rust compiler should be doing this automatically This will become a lot easier once the actual full const-generics implementation lands. Because then even your "non-generic" implementations will be enabled to depend on compile-time-defined features of the instantiated type (such as size, alignment, value bounds etc.) without any sort of duplicate code generation. Of course, these in turn should delegate to truly non-generic code when feasible to minimize bloat.
- thinkafterbef 5y agoAwesome work! If you want to cut the CI run time even further. I'd recommend BuildJet for Github Action. We give you managed high-performance CI runners, which cuts both your CI time and price in half. We plug right into Github Actions, so it's just one line change in your yaml file. https://buildjet.com/for-github-actions https://buildjet.com/for-github-actions
- smoldesu 5y agoI'm not an expert on the language, but I see Rust's build times as fairly justified. Rust's focus is on execution speed and correctness, which means that the compiler needs to be extremely pedantic with CPU targets to know how to optimize your release. Once you have a target outlined, it has to acquire and build the latest versions of your dependencies with the highest level of optimization possible for your specific architecture... that's just a lot of computing, period. Caching is a half-solution to this as the author suggests, but frankly there isn't much juice left to squeeze out of the Rust fruit. Now, there are still ways to make the compiler choke: the mention of instantiations towards the end is a great example. But in my (non-professional) experience, the build times are pretty tolerable for what it's providing.
- ReactiveJelly 5y agoThe critics are right that the build times are slow even in debug mode, and that it's one of the most frustrating things about the language even for experienced users. There are some alternate codegen backends that aim to fix this by compiling less-optimized debug builds more quickly.
- sodality2 5y agoOoh, got a link to any of those alternate backends?
- pjmlp 5y agoFor example Cranelift and using lld as linker. Also read the Bevy documentation about improving build times.
- PoignardAzur 5y agoI wouldn't hold my breath too much. Something I noticed since I've started timing builds is that codegen and linking usually takes less than 30% of the build time for an incremental build. So in practice, switching to Cranelift and lld/mold will shave maybe 2 seconds off a 10-seconds build. Better, but still flow breaking. If we want Go-like compile times, we're going to need some more fundamental improvements to the compiler.
- justinsaccount 5y agoI tried again to use rust for something the other day. Just wanted to fetch some things via http. Nothing fancy, didn't even really care about the response body, just the timings. Consensus seemed to be that reqwest is the way to do this using rust, since the stdlib does not include an http client. Adding the dependency on reqwest tried to pull in over 100 dependencies and after building for a while, it failed somewhere along the way because my rust version was too old. Now, maybe some of these were optional dependencies, but they all got pulled in automatically. so, when TFA says > Another obvious advice is to use fewer, smaller dependencies. I just went back to go, which can do the crazy obscure task of making an http request without requiring 100 external dependencies.
- ReactiveJelly 5y agoYeah Go just has those as internal dependencies in the stdlib. That _is_ the tradeoff - Rust builds up from nothing and makes it friction-less to add a new dependency. Go starts at a higher level and the friction for adding new stuff is high. And some of the high-level stuff, like the GC, can't be removed.
- lumost 5y agoRusts ability to squeeze into small footprints is a huge part of the appeal. I don’t think that that excuses the exceptional difficulty of writing a web server or client in rust relative to other languages. How often are folks writing applications that don’t need to connect to the web these days?
- kibwen 5y ago> How often are folks writing applications that don’t need to connect to the web these days? For Go, perhaps rarely. For Rust, not needing to touch the web is extremely common. Let's also keep in mind that Rust's stdlib does include IP, TCP, and UDP.
- at_compile_time 5y agoI just tested this. It took me 43 second to build a new project with reqwest as a dependency. The second build took 0.55 seconds. If that's exceptional difficulty, I'm underpaid. Maybe the lesson here is to update Rust if you haven't used it in a while?
- eminence32 5y ago> Another obvious advice is to use fewer, smaller dependencies. As a library author, I sometimes wonder if I should be introducing more feature flags into my crate to let people only use the bits that they want, in order to help compile times. The whole thing doesn't take very long to compile (~20 seconds), but maybe the added complexity of juggling feature flags might be worth it.
- nicoburns 5y agoYou think 20 seconds isn't a long compile time!?
- axiosgunnar 5y agoI don't see the problem. Since it's an external crate, it will compile exactly once after adding it to Cargo.toml, and will be cached forever.
- unanswered 5y agoI'm convinced that most of the people complaining about build times are using docker for some incomprehensible reason and are blowing away the dependency cache with every build.
- rowanG077 5y agoThis. It's annoying to compile from scratch. But I almost never have to do that. So I don't really have a problem with compile times.
- nicoburns 5y agoNope. FWIW, I conpile on a MacBook. So I only have 2 cores, and laptop ones at that. Perhaps that's affecting our difference in perspective? I also had a project a couple of years ago where I had to compile it in release mode to get reasonable performance for testing (Iw as processing GB's of data), which meant 2-3min incremental compile times. Not fun!
- pjmlp 5y agoThe irony of cargo not supporting binary libraries is that the usual solution ends up reducing the number of dependencies, and being forced to rewrite ourselves the solution in another way.
- kibwen 5y agoIf you want to set up something like this for your own use, look into sccache. Integrating this more broadly into cargo would be generally pretty useless without a stable Rust ABI.
- pjmlp 5y agoYeah that is a possible solution, but still not quite what vcpkg, conan and distribution repositories offer, or having COM/Windows Runtime libraries. I look forward when cargo offers that as an option, doesn't mean everyone has to offer their libraries that way, just having the possibility on crates.io would be nice. I am also very thankful for what has been achieved thus far, so don't take this as thankless complaint rather as I would like Rust one day to be.
- jgavris 5y agoI'm surprised nobody has mentioned trying out using Google's Bazel build system with the Rust rules. Bazel provides relatively straightforward 'remote caching' of build artifacts via Google Cloud Storage or S3. https://github.com/bazelbuild/rules_rust https://github.com/bazelbuild/rules_rust
- gravypod 5y agoAnother few benefits with Bazel is that your build is mostly hermetic and reproducible. I've never had to run `bazel clean` and the output sha of your builds will always be the same (good for signing and security). You can also use remote build execution (RBE) to run your builds in parallel. This makes for a massive performance boost when you're compiling a lot of libraries (packages, source files, etc). It also models your source code in your build system (exposing libraries, binaries, etc). I'd love to see a performance comparison with `rules_rust`.
- jgavris 5y agoOf course, making the right abstractions and library / module / crate boundaries is still very much important to incremental build performance. For 'very large' Rust projects, setting up remote build execution can be hard, but a few of the original Bazel folks at Google are trying to make that easier for folks with EngFlow https://www.engflow.com/ https://www.engflow.com/
- gravypod 5y agoThis space is pretty healthy right now! For an updated list of people in this space you can also check out: https://bazel.build/remote-execution-services.html https://bazel.build/remote-execution-services.html
- finnb 5y agoEngflow has some very smart people. There are also a number of other open source alternatives such as Buildbarn (https://github.com/buildbarn https://github.com/buildbarn) and Buildfarm (https://github.com/bazelbuild/bazel-buildfarm https://github.com/bazelbuild/bazel-buildfarm) which you can host yourself. Personally, I've found Buildbarn great to throw up in Kubernetes and build massive projects with.
- carlmr 5y ago>[Build time] also is a non-linear factor. Just waiting for the compiler is the smaller problem. The big one is losing the state of the flow or (worse) mental context switch to do something else while the code is compiling. One minute of work for the compiler wastes more than one minute of work for the human. This is so true, and so hard to convey to a lot of people, even in engineering. They like to abstract humans as machines that can just context switch at a whim, which is just not true.
- dhosek 5y agoI'm old enough that I can remember having to wait 45 minutes for a compile to finish and not be able to do anything else with the computer. The worst cases of this were the slow processes which required occasional manual intervention. Or, the situation where if there was an error during compilation, only the first error was reliably meaningful and everything after that might or might not be an accurate reflection of the state of the source code. It's worth noting that there's a sort of uncanny valley in the amount of time something takes: when it's short, it's not too big a deal to wait for it. When it's fairly long (and predictably so), it's easy enough to context switch (I used to read a lot waiting for compiles to finish). It's the stuff in the 5–10 minute range that kills you. What depresses me, is that while I can now do something else in a different window while a process runs, the issues with errors and requiring interaction still persist.
- hardwaresofton 5y agoMost building systems offer a way to cache build results (ex. gitlab), so the largest speedups usually come from there. Another way is to use an explicit “builder image” with packs your dependencies and an older version of the program into the starting point for the run. Yet another way is to just use bigger machines if you’re CPU bound. Github and Gitlab generally offer 1/2 core 3-8GB RAM instances, but SurplusCI[0] (a project I recently launched) has no such restriction. Up to 8 vcore 32GB RAM machines available. [0]: https://surplusci.com https://surplusci.com
- cropcirclbureau 5y agoMore tips for improving compilation times: https://endler.dev/2020/rust-compile-times/ https://endler.dev/2020/rust-compile-times/ Personally, dynamically linking dependencies and using the `mold` linker has given me the most bang for my bucks. You can't dynamically compile all crates (actix_web, sqlx) unfortunately due to some assumptions macros make about crate paths (using Bevy scheme). And this exclusion extends to all crates pulled in as [dev-dependencies].