1. 51
    1. 7

      it takes advantage of compilation model based on proper modules (crates)

      Hang on a second, crates are not ‘proper’ modules. Crates are a collection of modules. If any file in the crate changes, the entire crate needs to be recompiled.

      ‘Proper’ modules are ones where the unit of compilation is the file, and the build can be parallelized and made much more incremental.

      The first advice you get when complaining about compile times in Rust is: “split the code into crates”.

      When files are compilation units, you split the code into files, which is standard development practice.

      Keeping Instantiations In Check…If you care about API ergonomics enough to use impl trait, you should use inner trick — compile times are as big part of ergonomics, as the syntax used to call the function.

      I think something has gone off the rails if, in the normal course of writing code, you need to use sophisticated techniques like these to contain build times. Hopefully it gets better in the future.

      Great article though.

      1. 10

        Yeah, agree that calling something “proper” without explaining your own personal definition of proper is wrong. What I’ve meant by “proper modules” is more-or-less two things:

        • code is paresd/typechecked only once (no header files)
        • there are explicit up-front dependencies between components, there’s no shared global namespace a-la PYTHONPATH or CLASSPATH

        So, I wouldn’t say that pre C++20 compilation model has “proper modules”.

        That being said – yes, that’s a very important point that the (old) C++ way of doing things is embarrassingly parallel, and that’s huge deal. One of the problems with Rust builds is that, unlike C++, it is not embarrassingly parallel.

        I’d actually be curious to learn what’s the situation with C++20 – how template compilation actually works with modules? Are builds still as parallel? I’ve tried reading a couple of articles, but I am still confused about this.

        And yeah, it would be better if, in addition to crates being well-defined units with specific interfaces, it would be possible to naively process every crate’s constituting module in parallel.

        I think something has gone off the rails if, in the normal course of writing code, you need to use sophisticated techniques like these to contain build times.

        To clarify, there’s an or statement, in normal application code one should write just

        pub 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)
        }
        

        There’s no need to make this template at all, unless you are building a library.

        But I kinda disagree with the broader assertion. Imo, in Rust you absolutely should care about compile times when writing application code, the same way you should care what to put in a header file when writing C++. I think we simply don’t know how to make a language which is both fast to run and fast too compile. If you chose Rust, you choose a bunch of accidental complexity, including slower built times. If you don’t care about performance that much, you probably should choose a different language.

        That being said, I would love to read some deeper analysis of D performance though – my understanding is that it, like C++ and Rust, chose “slow compiler” approach, but at the same time compiles as fast as go? So maybe we actually do know how to build fast fast to compile languages, just not too well?

        1. 2

          I’d actually be curious to learn what’s the situation with C++20 – how template compilation actually works with modules?

          I believe pretty much like in Rust: templates are compiled to some intermediate representation and then used during instantiation.

          Are builds still as parallel?

          No, again the situation is pretty much like in Rust: a module interface is compiled into BMI (binary module interface; equivalent to Rust’s crate metadata) and any translation unit that imports said module cannot start compiling before the BMI is available.

          I also agree that C++20 module’s approximate equivalent in Rust is a crate (and not a module).

          BTW, a question on monomorphization: aren’t argument’s lifetimes also turn functions into templates? My understanding is that while in C++ we have type and value template parameters, in Rust we also have lifetime template parameters which turn Rust into an “almost everything is a template” kind of language. But perhaps I am misunderstanding things.

          1. 5

            No, again the situation is pretty much like in Rust: a module interface is compiled into BMI (binary module interface; equivalent to Rust’s crate metadata) and any translation unit that imports said module cannot start compiling before the BMI is available.

            Thank you! This is much more helpful (and much shorter) than the articles I’ve mentioned.

            BTW, a question on monomorphization: aren’t argument’s lifetimes also turn functions into templates

            That’s an excellent question! One of the invariants of Rust compiler is lifetime parametricity – lifetimes are completely erased after type checking, and code generation doesn’t depend on lifetimes in any way. As a special case, “when the value is dropped” isn’t affected by lifetimes. Rather the opposite – the drop location is fixed, and compiler tries to find a lifetime that’s consistent with this location.

            So, while in the type system type parameters, value parameters and lifetimes are treated similarly, when generating machine code types work like templates in C++, and lifetimes roughly like generics in Java. That’s the reason why specialization takes so much time to ship – it’s very hard to make specialization not depend on lifetimes.

        2. 1

          Well your definition is interesting, because I actually don’t (exactly) agree.

          • code is paresd/typechecked only once (no header files)

          In OCaml you have ‘header’ or rather interface files, which are parsed/typechecked only once. They’re the equivalent of C++ precompiled headers except the compiler does it automatically.

          • there are explicit up-front dependencies between components, there’s no shared global namespace a-la PYTHONPATH or CLASSPATH

          Again in OCaml, there are dependencies between modules, but they are implicit. They just fail the build if modules are not compiled in the right order with the right dependencies. Fortunately the build system (dune) takes care of all that.

          Also OCaml has a shared global namespace–all source code files automatically become globally-visible modules within the project. Again fortunately the build system provides namespacing within projects to prevent name clashes.

          Another example of ‘proper modules’ is Pascal’s units, which actually satisfies both your above criteria (no header files, and explicit dependencies between units), and provides embarrassingly-parallel compilation.

          I think we simply don’t know how to make a language which is both fast to run and fast too compile.

          That may well be true.

          D

          From what I’ve heard, D compiles fast. And I assume it runs fast too. OCaml is pretty similar e.g. in some benchmarks it has similar single-core performance to Rust.

          1. 1

            Yeah (and here I would probably be crucified), I’d say that OCaml doesn’t have proper modules :-) Compilation in some order into a single shared namespace is not modular.

            Rust’s approach with an explicit DAG of crates which might contain internal circular dependencies is much more principled. Though, it’s sad that it lost crate interface files at some point.

            1. 1

              I’d say that OCaml doesn’t have proper modules :-)

              Heh, OK then ;-)

              Compilation in some order into a single shared namespace is not modular.

              That’s exactly what Rust crates end up doing. It just shifts the concept of ‘namespace’ into the crate names. Same with Java, C#, etc. Rust:

              use serde::{Serialize, Deserialize};
              

              OCaml:

              Serde.Serialize.blabla
              
              1. 2

                It just shifts the concept of ‘namespace’ into the crate names

                Not exactly: Rust crates don’t have names. The name is a property of the dependency edge between two crates. The same crate can be known under different names in two of its reverse dependencies, and that same name can refer to different crates in different crates.

                1. 1

                  I think this is a distinction without a difference. Rust crates have names. They’re defined in the Cargo.toml file. E.g. https://github.com/serde-rs/serde/blob/65e1a50749938612cfbdb69b57fc4cf249f87149/serde/Cargo.toml#L2

                  [package]
                  name = "serde"
                  

                  And these names are referenced by their consumers.

                  The same crate can be known under different names in two of its reverse dependencies

                  But they have to explicitly rename the crate though, i.e. https://stackoverflow.com/a/51508848/20371 , which makes it a moot point.

                  and that same name can refer to different crates in different crates.

                  Same in OCaml. Different projects can have toplevel modules named Config and have them refer to different actual modules. If there is a conflict it will break the build.

                  1. 2

                    If there is a conflict it will break the build.

                    The build will work in Rust. If, eg, serde some day publishes serde version 2.0, then a project will be able to use both serdes at the same time.

                    So, eg, you can depend on two libraries, A and B, which both depend on serde. Then, if serde published 2.0 and A updates but but B does not, your build will continue to work just fine.

                    Not sure if OCaml can do that, but I think Java (at least pre modules)/C/Python can’t, without some hacks.

                    1. 1

                      That is a design choice. OCaml makes the choice to not allow duplicate dependencies with the same name.

      2. 4

        What do you mean when you say ‘proper’ modules?

        I understand the author mean a compilation unit, so a boundary around which you can count on compilation being separable (and therefore likely also cacheable). In C and C++, that happens to be a file (modulo some confusion about the preprocessor). In Rust, it is a collection of files called a crate. Once you accept that, everything else you say about file-based compilation units holds for Rust crates too: you can parallelize compilation of crates, you can cache compiled crates, and you get faster compile times from splitting code into crates.

        1. 1

          I defined ‘proper’ modules in my comment second paragraph.

    2. 5

      Followed the suggestions, cut end-to-end time (when the cache works) from 20 minutes to 11 minutes.