Thoughts on Rust bloat
raphlinus.github.io
raphlinus.github.io
For instance, http client and server libraries are often in this gray area of uncertainty about whether they should be in the stdlib or not. Is this something language designers or compiler implementors have a lot of experience in? I would say not; sending and serving http requests are not something compilers need to do. Or take GUI libraries, what do language designers know about that?
I also know of many bad third party libraries, but there are tons of examples of really awful parts of standard libraries. The original date/time APIs in Java are a mess (they finally fixed this by in essence bringing in a third party API). The ssl bindings in the Ruby stdlib were a common source of bugs back when I was paying attention to this (maybe they've fixed it), same thing for the built in http stuff. Someone else mentioned the similar weakness of python's built in http, such that most people use a third party library instead. Even the java collections APIs are pretty poor such that people often augment them with things like guava or apache libraries.
My point is just that developing good libraries is a hard thing and I don't see any reason to think language designers or compiler maintainers are any better (or worse!) at it than other people. There isn't really a shortcut, you can't just cede authority to the powers that be on the core language teams, you just have to evaluate the quality of libraries for your use case yourself.
I think the size of the standard library is just one of possibly many contributing factors that leads to a large number of dependencies. I think a part of it is culture, but another part of it is that the tooling _enables_ it. It's so incredibly easy to write some code, push it to crates.io and let everyone else use it. That's generally a good thing, but it winds up creating this spiral where there's almost no backpressure _against_ including a dependency in a project. This means there's very little standing in the way of letting the fullest expression of DRY run wild. There are some notable examples in the NPM ecosystem where it reaches ridiculous levels. But putting the extremes aside, there's a ton of grey area and it can be pretty difficult to convince someone to write a bit more code when something else might work off the shelf. (And I mean this in the most charitable way possible. I find myself in that situation.)
I do hope we can turn the Rust ecosystem around and stop regularly having dependency trees with hundreds of crates, but it's going to be a long and difficult road. For example, not everyone even agrees with my perspective that this is actually a bad thing.
Thus a lot of things get done in Python and Java, because of the ample standard libraries. I was able, after a year of lobbying and procedures and approvals, to get a Rust compiler, but there is zero chance of me ever getting anything off crates.io.
.NET is an interesting case, because of WinForms, which is a de facto part of the standard library. I think few C# developers think of WinForms as bloat; it's very convenient for making simple UIs. Yet putting, say, GTK+, in the Go standard library would doubtless be considered bloat. I don't think there are easy answers to these questions.
It's not that convenient on Linux though...
I have more thoughts, but they are very hand wavy and ill-formed, so please take them with a grain of salt. One of my theories for why the Go standard library has had as much success as it has, is that it doesn't necessarily provide implementations that go as fast as reasonably possible, and that tends to give more flexibility for exposing simpler APIs. A good microcosm of this idea is JSON (de)serialization. Without even blinking, I can think of three reasonably popular third party JSON (de)serialization libraries in the Go ecosystem. The one provided by the standard library is pretty slow compared to some of them. It's not clear to me that it can be fixed without changing the API. But it's a good example where the standard library has provided something, but it isn't good enough in a lot of cases, so folks wind up bringing in a third party dependency for it anyway.
But even that alone isn't necessarily a bad thing. encoding/json is likely good enough for a really large number of use cases. On top of that, it's very convenient to use. (I'd still take serde in Rust over Go's system any day, but that's a different conversation.) And this kind of fits within Go norms pretty well. Go was never built to be the fastest, so the fact that some of its standard library has perhaps sacrificed performance for some API simplicity is totally consistent with that norm. And I don't think that norm is a bad thing.
There are some other examples where Go's standard library is slower than what it could be, for example, CSV parsing and walking a directory hierarchy.
Overall, I think the balance struck by Go's standard library was very nicely done. However, I'm not convinced it could have been replicated by Rust. The reasons for that are just guesses, and wander too far into musings about how the language is itself developed. But even putting that aside, Rust is going to have stricter requirements, because people tend to gravitate toward Rust when performance is important. So if std doesn't provide the fastest possible thing, then it's going to be a bigger deal than if Go does the same thing.
Again, above is super hand wavy and just a bunch of opinions from my own personal perspective.
It feels to me more like code incidental to other purposes than a library-as-library. Some folks like that and extracting from a concrete use is often instructive and efficient.
On the other hand, there is a truly maddening inconsistency in whether you get an interface or a physical struct.
One of these lends itself to easy replacement, injection and mocking.
The other lends itself to writing the nth-tillion interface wrapper for the parts of the standard library which use physical structs. Which are, naturally, slightly different from and therefore incompatible everyone else's bangzillionth interface wrapper for the parts of the standard library which use physical structs.
Then there's errors. But that's another day's rant.
That’s an issue, though it’s also one with most Go libraries too.
The thing is though, unnecessary interfaces also kind of suck. It makes code harder to follow, when the concrete type is hidden behind an interface. Also, one of the things that’s common in Go is testing with real implementations, rather than mocking - and I used to do just that. Even with Redis, I had a tiny shim server that implements the Redis protocol, that I would use in tests. Not absolutely everything can be done efficiently this way, but the virtues of testing with real clients are hard to ignore. Mocks and stubs can hide a lot of bugs that you would need to hope are caught by slower integration or e2e tests... And by virtue of being slower, they generally would cover less branches, too.
> Then there's errors. But that's another day's rant.
Have you kept up with the latest? I think they’re headed in the right direction with errors. Specifically with Is and As, along with the %w directive. The %w directive is a bit weird, but honestly, it’s a clever solution, and it seems like it would work.
Are you suggesting including the core `rand` crate with some traits and a basic pseudo-random generator in the stdlib, and allowing other crates to use those traits to implement the many other[1] RNGs?
I'm honestly not sure this is a good idea, it seems the rand crate has undergone a few iterations, crystalizing this inside the stdlib might lead to problems?
What is far better and faster than SHA3?
And while SHA2 wasn't designed for that use, it's easy to make simple and provably correct constructs that turn a secure hash into a secure RNG.
I would definitely not want my std to be designed this way.
2. You definitely don't want your std to be designed with a simple API to a fast, secure, and popular crypto primitive? Pretend I named your favorite one, to avoid bikeshedding issues.
I get that security people don't want non-secure RNGs to exist out of fear that someone might try implement security related functions out of them but why should I care if I just want to choose between 3 types of enemies this wave?
[0]: https://doc.rust-lang.org/std/collections/hash_map/struct.Ra...
[1]: https://docs.rs/rand/0.7.0/rand/rngs/struct.ThreadRng.html
Javascript's Math.random and Crypto.getRandomValues works this way.
> Be aware that the underlying Rust language will continue to evolve. Syn is able to accommodate most kinds of Rust grammar changes via the nonexhaustive enums and Verbatim variants in the syntax tree, but we will plan to put out new major versions on a 12 to 24 month cadence to incorporate ongoing language changes as needed.
With std I am bounded by the current language version.
Having a defacto standard library has more issues IMO than a real standard library.
With that said, if `syn` finds itself in a spot where it needs to do a breaking change release for a new language feature, then that would be tricky. It sounds like syn's architecture is pretty flexible (non-exhaustive enums), but it's not clear to me that it could support all possible language additions without breaking changes.
Trustworthy is another thing altogether though. How do you handle this process in C and C++? Both of those languages have fairly spartan standard libraries as well.
I just did our license audit and 100% of our shipped deps were Apache | MIT. If you can clear those two licenses with legal, you should be good to go for virtually any crate.
Might be worth releasing my one liner for this if anyone else finds it useful.
And you usually see them cursing the platforms that don't care about POSIX support.
And ISO C++ has repented themselves from following C's footsteps and have been improving the standard library since C++11, mostly by integrating boost libraries.
Also, while some of your historical context is interesting, I don't really appreciate your editorialization, which I often find is off the mark personally.
So in reality POSIX complements libc as C's "runtime platform", even though ISO C never considered to make libc that big.
My historical context is how I experienced how things went, throughout the media we had available at the time.
I surely welcome factual corrections when I fail off the mark.
Everyone benefits from learning proper history facts.
I wasn't. Even with POSIX, it is spartan by today's standards. Look at the standard libraries of Python and Go. POSIX doesn't have JSON (de)serialization, HTTP servers, XML parsing and a whole boatload of other crap. So I don't think there is anything wrong with my characterization.
That is the ultimate problem of batteries includes vs a small standard library distilled into a specific example.
People used to tout Python's "batteries included" line as positive almost universally a decade and more ago. Then better replacements were developed, and those batteries started looking less appealing.
There's really no escaping that while including any base functionality. Either you include it and portions will be stale later as better interfaces and paradigms are developed, or you don't, and you risk slower adoption and harder usability as people need to figure out solutions for common tasks, even if that solution is as simple as find the crate that provides it. Because eventually that becomes a problem not of finding the crate, but finding the best crate out of the multiple that exist, which itself causes fragmentation of the common developer experience (and thus makes it harder to share knowledge and have a good community).
A case in point is how the ORM Entity Framework that comes with .NET has made the older NHibernate (a separate package) obsolete.
.NET developers love using the standard libs, but OTOH Microsoft has a lot of resources to create very complete libraries.
In theory they can move into the standard library, but none ever have. The process exists though.
Another example was CompletableFutures which were inspired by ListenableFutures from Guava.
I can now use these with guarantees that they will be stable as Java has strong commitments to backwards compatibility.
I may not be a typical .Net developer, but my personal feeling is that this is too general and somewhat glib.
If you're inexperienced you will (and should!) choose the default option, if there's one available, and the most commonly used option if there isn't a default. So, if you wanted an ORM, then before EF there was NHibernate. But after a while you become able (from painfully gained experience) to determine what you want from a tool and what trade-offs you want to make, so you might use something like Dapper or no ORM at all.
Personally, I have found some of the MS provided implementations - shall we say - less than optimal. The Unity Framework. EF (which I dearly wish I have never had to suffer using). MSTest. Enterprise Library (oh god ugh).
So I make other - informed - choices about what to use. Some things I keep using because they are 'good enough' and I have a library of utilities and a mental map of how they work - log4net, NUnit - and some things I find I just don't need any more (mocking libraries, for one).
MS still support their provided implementations - as they should - but even their own projects can and do use third-party frameworks rather than the MS provided implementation (for example, Bot Framework used AutoFac (back when I was using it, anyway) [1]) - because their developers have been released from the requirement to exclusively use MS provided frameworks, and are making their own choices about what to use in their projects, and consequently what their users should use when using those tools.
Eventually, of course, if a tool is so crucial that it becomes part of .Net itself - the best example I can think of is dependency injection in .Net Core, which is in Microsoft.Extensions.DependencyInjection - you'd have to be mighty stubborn to use anything else.
One other point: I wouldn't describe NHibernate as 'obsolete', but given their historic and chronic inability to keep their documentation sites live and working ([2] referenced from [3]) it's easy to get that impression. But people are certainly still using it [4]. Just not as many as there used to be.
[1] https://github.com/microsoft/botframework-sdk/issues/938 [2] http://www.nhforge.org/doc/nh/en/index.html [3] https://ayende.com/blog/4139/nhibernate-documentation [4] https://stackoverflow.com/questions/tagged/nhibernate
This did happen over time, but NHibernate was still really popular for a long time after EF came out, because of limitations it had.
I also don't think it was entirely because EF existed - over time, EF implemented more and more features that NHibernate had, yet at the same time it seemed like the NHibernate team had given up - there were no updates to it for a long time. It was the lack of updates that moved me to EF, but I always preferred NHibernate.
I won’t add a library to our stack if we have an existing solution.
Most of our apps are written in C# and use Entity Framework with either ASP.NET MVC or Windows Forms.
I would need an excellent reason to use something else.
We don't have to just guess or make biased claims about what would be useful to move to the standard library. We can look at the numbers and see what people are actually using.
These metapackage communities can then focus on interoperability of constituent packages w/o overburdening the standard libraries, and core language can remain lean and concise.
On point of the post, Tidyverse is a great deal of bloat by loading unnecessary packages instead of specifics and creating namespace issues where two functions have the same name. It's generally fine for interactive work, but causes so many issues in development as you can't pick and choose what's loaded into the environment.
In line with what you're saying, meta-libraries make sense in terms of developing a line of packages towards a singular vision, but only make sense when the core libraries aren't expanded regularly. Maybe in these cases more of push needs to be made to supporting the existing tools?
R is an odd example though, as the standard libraries are loaded by default and I think only really comprise of basic stats/graphics/data.frame tools. I don't really believe a great deal of extra tools have been added to base R in the last few years, just improved in terms of speed and memory use (e.g 3.5.0's change to compiled packages)
The advantage of a standard library is that you only need to learn one API instead of a dozen different APIs for doing the same thing, which means you can develop a degree of mastery over it. It also reduces the friction for using better abstractions. e.g. Every professional Python programmer knows defaultdict, whereas I rarely see that data structure used in other programming languages, it's too much of a leap to install a dependency to save a few if statements, but it all adds up.
The rust ecosystem has done well to converge on certain crates as sort of replacement for missing std features.
In practice (at least in the rust ecosystem), I only need to learn one interface for:
* regex (regex)
* serialization (serde)
* network requests (request)
There are de-facto base crates in the ecosystem.
Because that is the biggest asset from stuff being in the standard library.
To clarify, there are different levels of support on different platforms.
The standard library and 3rd party crates generally have excellent compatibility across mainstream platforms.
libc is barely a standard library, it's almost nothing.
C++ surely does include regex, with serialisation and http scheduled for C++23, or with luck with a TR as intermediate delivery.
Although serialisation depends on static reflection being finalized as well.
I think the definition is meaningless. Rust spends huge amount of resources to test itself and its surrounding ecosystem on tier 1 platforms.
I'm fairly certain regex from c++ std will run like crap, if at all on something with 4MB of RAM.
I am fairly certain that without profiling and defining a test configuration for a set of specific C++ compilers / standard C++ library I won't assert anything about std::regex performance with 4 MB of RAM.
In other words, not all functions in the stdlib of Ada, Pascal, C and C++ can be used in all possible target environments? Sounds like a failure to quality gate those standard libraries.
There aren't an endless number of profiles.
Each platform has different levels of support. Primary being Windows, Mac and Linux, where every pure Rust crate runs.
Std lib makes certain reasonable assumptions, for which it works e.g. malloc exists and panic! is implenented.
Looking at crates.io, regex looks pretty safe, as it’s authored by “The Rust Project Developers” and includes explicit future compatibility policies. Unfortunately, I can’t find an index of only the crates maintained by the Rust team.
Serde is obviously popular, but at first glance is a giant Swiss Army knife that will likely have lots of updates to keep track of that are completely unrelated to my project (whatever it is). If I search for JSON, I get an exact match result of the json crate, followed by a bunch of serde-adjacent crates, but not serde itself.
Request hasn’t been updated in 4 years, and has a total of less than 7000 downloads.
Not everyone or every project will have the same desires, though. Sometimes, a fast-moving experimental library is the right choice. The trouble is figuring out which I’m looking at.
If you'll want to update to always be on the latest version of each crate, well that discomfort about them potentially not working is part of the price.
Not everyone does, and that’s fine. I just want to know what a library developer’s stance on it is before I try to use their library.
I agree that this should be better documented and probably more integrated with crates.io somehow.
All these libraries are very well known within the community and are what I would come up with as a complete outsider (I don't think I've written more than a hundred lines of Rust code to this date).
You can also find some pointers here:
When talking of the standard library, static typing doesn't save you when breakage happens. It's better of course, at least the compiler protects you from obvious errors (although that doesn't work for transitive dependencies, with the dynamic linking to binaries that Java / the JVM does ;-))
The problem is when a piece of code that was compiling fine a year ago, fails to compile on a newer version of the standard library, due to breaking changes, that's going to take time and effort to fix.
And this gets worse when the breakage happens in dependencies and those dependencies are no longer maintained. This can always happen of course, not just due to the standard library, but due to transitive dependencies too. But still, breakage in the standard library, or in libraries that people depend on, is a bad thing. And consider that as the number of dependencies grows, so does the probability for having dependencies that are incompatible with one another (compiled against different versions of the same dependencies).
And semantic versioning doesn't work. Breaking compatibility will inflict pain on your downstream users, no matter how many processes you have in place for communicating it. And this is especially painful when you're talking about the standard library.
If the standard library introduces breaking changes, regardless if the language is static or dynamic, then it's not a standard library that you can trust. Period.
Also — when should you break compatibility, in the standard library or in any other library?
The answer should be never!. When we want to change things, we should change the namespace and thus publish an entirely new library that can be used alongside the old one. Unfortunately this isn't a widely held viewed, but I wish it was.
---
Going back to the batteries included aspect of some standard libraries, like that of Python, there's one effect that I don't like and that's not very visible in Python since the bar is pretty low there.
The standard library actively discourages alternatives.
When a piece of functionality from the standard library is good enough, it's going to discourage alternatives from the ecosystem that could be much better.
Some pieces of functionality definitely deserve to be "standard". Collections for example, yes, should be standard, because libraries communicate between themselves via collections. And that's what the primary purpose of a standard library is ... interoperability. Anything else is a liability.
https://docs.oracle.com/javase/8/docs/api/java/util/Map.html...
I also remember from years ago that Bundler in Ruby allowed version mismatches to coexist and would install both deps in the same tree, punting any runtime issues (same as NPM).
Any Gentoo or NIX users might have something to add here, as I can remember being regaled at conferences by them about this topic.
All that said, it would seem that Rust could have some kind of super-intelligence about dependencies due to all the static goodness. So my questions are:
a) who cares how much is userland and how much is std lib if everything is equally safe and documentable?
b) if "too much" becomes std lib, could any feelings of overwhelmingness not be mitigated with more namespacing?
But in the modern world, I agree with the small language people: the logistical problem is dead. Almost every modern language has push-button dependency management, so the difficulty overhead of using a third-party library is basically zero. And, as you pointed out, this means that the stdlib now competes with third-party libraries, and the third-party libraries proliferate wildly. So there's still a problem, it's just not logistical anymore.
The other problem a stdlib solves is making decisions. This is the npm nightmare: which of these fifty libraries do I use? They're easy to install, but hard to evaluate. This compounds because every library is also making those decisions with their dependencies and so on, so you can end up with 8 different implementations of basically the same thing. You don't have this problem with a stdlib because if there's a std::string, everyone expects your code to work with std::string.
So perhaps the modern stdlib is better off as a standards library: no code, just a mapping from name (ie "option_parser") to package@version (ie "clap@1.1.0"). The work of the stdlib would then be to curate this mapping in a cohesive way, so the packages are high quality individually, but also work well together and reflect the general direction of the language and its community. Whether or not these packages are actually bundled with the compiler is really just an implementation detail. The library isn't code; it's decisions.
Updates would be pinned to release versions, so backwards-incompatible changes coincide with a major version/edition/etc of the language. This would be an expected process, because, as with urllib{n+1}, the first answer isn't always the best. Third-party packages are just as available as ever, of course, and a destandardised package is only a (trivially automated) rename away.
Aside from providing a layer of simplicity and stability over the third-party ecosystem, the standardised libraries would serve as reference implementations for a common interface, like Node's express/connect, or Rust's tokio/futures. In other words, it could help concentrate community effort around emerging standards.
I grant that this is a very, uh, political approach to what has historically been a code problem, but if anything I think the current situation owes itself largely to treating community problems ("how do we agree on a foundational layer of library code?") with technical solutions (package registry + sort by stars).
For a language like Rust this looks different..
That beeing said, I think the whole Rust userbase would benefit from having some sort of collection of well tested crates and strictly divide between private, work in progress and production crates.
We used to run a whole gamestate of a shipped title on PSP in a 400kb block allocation. I've yet to see that in any other dynamic language of consequence.
One of the ideas I wanted very much to explore is scaling the API, both up and down. For building something akin to PyPy, you might want a 'kernel', a small set of libraries that were available everywhere, and several other levels that include more or different things.
Mobile takes the base and adds a few things suitable for mobile (storage, UI, broader networking). Desktop has a real UI, and then there's the kitchen sink like .Net and Java have.
But you have the same problems you always have with decomposition - if you didn't guess the right boundaries when you built the thing then removing or rearranging bits is a serious PITA. Sometimes I think the best we can hope for is to leave clear messages for the next language so that it doesn't organize things the way we did.
I think this was only possible because Lua itself is such a simple language. It says a lot that LuaJIT's implementation of this simple language is extremely complex in comparison to vanilla 5.1. Imagine the added complexity for something like Ruby that offers more than one associative data structure.
With LOVE you get a complete game engine runtime with all the boring OS abstractions taken care of and you can just start working.
I tried a few game development bouts with Rust since it seemed like a natural step up from C++ but the compile cycle kills a lot of my creative drive.
I think this is just a consequence of the language being compiled, as with C++. With Rust having a wide variety of language features the compile time is increased also. At least we're lucky to have languages today that offer far shorter turnaround times for building prototypes.
I propose that the dictionary entry for 'faint praise' be updated to include this example.
Rust has a much broader aim, so a stdlib accommodating all of its usecases would be as comically huge as Python's, with all the problems that come from that.
"a stdlib accommodating all of its usecases would be as comically huge as Python's, with all the problems that come from that."
That's a straw man. I don't want a stdlib like Python's, I want one like Go's.
I could pin every dependency, and monitor CVEs for all my dependencies, and monitor the ownership/code changes for all my dependencies, and hope for updates in a timely manner should there be CVEs... or I can use the standard library and move on with my life.
Personal opinion of course. Some people enjoy monitoring CVE lists. :)
Completely agree. I hold firm to the believe that a strong standard library that considers modern software development goals will drive the success of a language.
Look at Go. Which is usually my example since not many do what Go does. You can do a whole web application in Go with minimal use (if any) of external libraries.
Back to Rust and their defense: they intend to adopt popular / quality packages from Cargo into the standard library or fill the gap between packages.
I secretly wish D had similar things to Go in the standard library. HTTP and SMTP etc which Python seems to have at least HTTP and sadly Go ditched their SMTP package for whatever reason making it awkward.
I hate having to learn a new package manager per new language. I love programming so I try a lot of languages out of love. Package managers and build systems are horrible UX in every language. I prefer to not rely on third party oh look now deprecated packages and start out with whats out of the box.
Rust does need a better package curation story, a Rust "expanded universe" of recommended packages whose provenance is more carefully tracked than the average package and whose APIs are stable. The Rust community needs to encourage other packages to depend on stable versions in that recommended set to reduce duplication.
But going further and actually making that set to be part of the "standard library", and thus released at the cadence of the Rust language itself, and managed under the same umbrella, would be harmful.
What they should've done is allow uploads only to "<username>/<cratename>". So if I decide to make a regex crate, it's called "majewsky/regex" at first and Alice can make "alice/regex" and Bob can make "bob/regex". Then at some point, the community (through some open process) decides that Alice's crate has the best API, so "alice/regex" gets aliased to just "regex".
That way, everything in the main namespace adheres to some sort of quality standard and has community support behind it. Because there's some explicit process gate to getting stuff into the main namespace, you could attach any number of beneficial requirements to it, e.g.:
- test coverage
- documentation coverage
- at least 3 people having committer rights to the repo, at least 2 of which must not be affiliated with the same company
But the underlying idea is sound: namespace and federation. Rust should ideally follow Java's solution. And it's completely forward portable: when namespacing is introduced, the crates.io/regex crates become io.crates.regex. Then you can have your com.github.majewsky.regex.
Libraries can be implemented by 3rd parties and maybe later adopted as standard libraries or become de-facto standards. That's how it has been done for most popular languages out there, and I think it's a smart decision.
I think it is a little ironic that he speaks of performance culture but simutaneously advises to use dynamic dispatch and avoid polymorphism. I can see the justification in non-critical code paths, but serialisation is a pretty important part of most networked software nowadays so I do not think that smaller binaries and faster compilation times (better developer experience) justifies a performance hit in the form of dynamic dispatch through crates like miniserde.
It's also worth noting that if you are using polymorphic functions that are only ever called in your code with a single known type parameter, then your program should be just as efficient as if you wrote a monomorphic version with that fixed type in both runtime _and_ binary size, which means the only disadvantage to the polymorphism there is compile time.
Recall that dynamic dispatch does not need you as a programmer to know which implementation is being used for a given polymorphic method or function - I find it difficult to see how the compiler would be able to reason what implementation is being referred to in code to generate static polymorphic code without the programmer being explicit. If that were the case, there would be no need to be explicit at all (and consequently no need for static dispatch in Rust code), and all you would need to do for polymorphism is to use dynamic dispatch. However, although this would be incredibly convenient and ergonomic, unfortunately the Rust compiler is not capable of magic.
However, I do believe that a competent engineer would have the judgement to be able to see, roughly, where performance hits would likely arise and optimise accordingly. Command-line arguments would likely not fall under this mandate, but serialisation to stdout is likely a good candidate for well-designed and well-optimised code. A nice side-effect is that this also avoids significant refactors down the line when you need to, in this case, change your serialisation from dynamic dispatch to static dispatch.
I see this assertion a lot, but I have never actually seen a system in which inlining that would otherwise be a win in terms of performance becomes a loss in a large system. LLVM developers seem to agree, because LLVM is quite aggressive in inlining (the joke is that LLVM's inlining heuristic is "yes").
I'd be curious to see any examples of I$ effects from the effects of inlining specifically in large systems mattering in practice.
It isn't exactly about inling but it is an example where optimizing for size also optimized for speed at the same time.
[0] https://dolphin-emu.org/blog/2014/09/30/dolphin-progress-rep...
[1] https://github.com/angea/pocorgtfo/blob/master/contents/issu...
The most likely people to be able to answer this one would be game devs or video codec hackers, at a guess.
I do know that inlining choices can have massive effects on executable size. I've seen more people complain about this kind of thing. It's most noticeable when controlling the inlining of a runtime library function in a language a bit more high level than Rust - I'm thinking of Delphi, with its managed strings, arrays, interfaces etc.
However, in terms of maintainability, I’m happy to foist a few extra kB of bytecode on developers to produce an idiomatic (i.e. serde-using) library.
I think if a problem can be solved with either, the default choice should generally be parametric polymorphism rather than subtype polymorphism, simply from a logical point of view.
Haskell is often described as passing around "dictionaries" corresponding to class instances. Presumably Rust could add the same functionality depending on how a type parameter is declared (eg, `fn foo<T: Foo>() ..` denotes a monomorphised function whereas `fn foo<%T: Foo>() ..` could denote a non-monomorphised function which at runtime takes arguments specifying the size and alignment of `T` as well as a vtable corresponding to the `Foo` trait). This would also make polymorphic recursion possible.
I have however thought that it should be automatically done based on heuristics, but since the notion of "zero-cost abstraction" is considered the default, I don't think this would be desirable.
In languages like Rust and Swift where values don't have a uniform representation (that is, not always a pointer), this takes a lot of infastructure, and a lot of performance/optimiser work to get reasonable performance for common code: the Swift compiler has quite a bit of code devoted to making generically-types values behave mostly like statically-typed ones, with minimal performance cliffs.
Rust's approach is that this sort of vtable-/dictionary-passing has to be done explicitly (with a trait object), and such values have restrictions.
I think I read somewhere that Javascript engines do something similar, with some extra code to de-optimize when you fiddle with the object prototype.
Wasn't apples to apples comparison, but it was some times faster at the time (my memory doesn't serve me, but something around 3x to 5x). Also, compilation speed went down (well, at the time :) ). It was mostly due how some of the features work in serde (flatten and tagged enums), though.
I made a separate, cleaner, experiment (https://github.com/idubrov/dynser), which does not show that dramatic improvement (again, wasn't apples to apples, there were other factors which I don't remember), but shows some.
Cargo needs to do better with shared caches (so you compile each dep at most once per machine) or ability to get precompiled crates (so you don't even compile it).
Incremental improvements of compiler speed or trimming of individual dependencies won't bring the 10x improvement it needs.
I agree though. I don't mind binary sizes in the single megabytes for a release build, and I don't mind clean/rebuild build times that are a few dozen seconds. I do mind the 500MB of build artifacts in a single target directory, multiplied by all the projects I have that share the same version of serde/winapi/syn/quote/insert-common-dependency-here.
The actual binary sizes are fine - even with embedded devices that have <1MB of program space, there doesn't seem to be much bloat. But I need to remember to clean every project when I finish working on it, because otherwise the build folders will eat 100s-1000s of megabytes each on a 16-32GB partition.
I really appreciate Rust's inclusive approach to learning and teaching, but I can't justify using it for education on ultra-affordable machines for that reason. People often scatter one-off projects all over the place as they learn, and when they run out of space it takes a long time to clean everything up.
So I would also be very in favor of some sort of simple shared package cache.
cargo build --target-dir ~/build-artifacts/$(basename $(pwd))
Then you can have something like this in your ~/.config/user-tmpfiles.d/clean_build.conf d /home/user/build-artifacts - - - 1d -
You can then do cleanup by calling systemd-tmpfiles --user
Or you can use the tmpfiles timer systemctl enable --user --now systemd-tmpfiles-clean.timer [build]
target-dir = "/home/user/build-artifacts"
But I don't know if the different projects will trample each other that way. If that's the case you could just go with a .cargo/config per-project targeting a subdirectory of the build-artifacts directory.I see a lot of difference between my i5 laptop and i9 desktop.
On upgrading from an OC i5-4670k to a r9-3900x, my compile times for a clean release build went from 20 minutes to 2.
It seems nalgebra is the culprit here: because Rust doesn’t yet support const generics, it has to use some hacky type-level metaprogramming to represent numbers, and that will definitely destroy build times.
The original code, after being migrated to an up to date version of Gtkmm, takes a couple of seconds with GCC 7, not more than one minute if at all.
The big difference? I don't need to compile from scratch all the 3rd party dependencies.
With every release from Rust I do a clean build to assert how much it has improved.
It was much worse, so congrats on the work achieved thus far, but it is still a pain to set up a project from scratch.
In the context of resource-constrained machines, one can always host it remotely on S3. (or mount an NFS share as the CARGO_TARGET_DIR, if you’re feeling adventurous or want fast CI)
NB: it works a bit better on not-Mac because staticlibs are deterministic.
https://github.com/rust-lang/rust/issues/61978
and its actually gotten worse since then, significantly worse. In the 2 months since then the installer has increased from 203 MB to 299 MB. Also unbelieveably, Rust has failed to address package balkanization which I would say has ruined the Node community. A popular package is "cargo-edit", which currently pulls in 239 other crates:
https://github.com/rust-lang/cargo/issues/2179#issuecomment-...
I'd say that this is more of a culture thing rather than a language thing.
2) Improve the metadata that describes a crate so that it is easy to tell if a crate should be used. For example, is the crate beta quality? Was a serious error found in the crate and it needs to be marked as "not safe"? Is it a Long-Term Support release? Etc.
3) As a culture, disallow trivial crates. No "is-odd" or similarly low effort crates. These just add bloat since the have so little functionality compared to their overhead. If your crate's toml is larger than the crate's code, you are doing it wrong.
So a language could:
- identify a blessed set of packages that don't imply separate trust relationships (a stdlib, or packages maintained or audited by the language team)
- require strong security practices and/or trust metrics from package authors
- clearly expose the full list of authors implicitly trusted by a requirements file
- offer community auditing tools, such as reproducible builds if delivering compiled files, web of trust tools, or diff tools
- run internal tests for suspicious packages (and don't talk about them)
If you frame the problem differently the solutions might be different, but I suspect there would still be a bunch of options.
And IMHO your issue does appear to be being taken seriously, at least judging from the link provided.
Balkanize definition: to break up (a region, a group, etc.) into smaller and often hostile or uncooperative units.
Are you implying there are competing crates with belligerent attitudes to each other?
> which I would say has ruined the Node community
I thought it was the basis and strength of node... (However I am too scared to use node or node toolchains: I fear trojans since I don't trust all dependencies).
The suggestion in this context is that if a library is replaced by several pieces, they will adopt different conventions, duplicate effort, not interoperate as freely, etc., instead of benefiting from a unified vision and design.
This is what I always wanted (for Rust as well as for C) but never got around to hack together myself. I dreamt it up more as a feature of cargo though, something like 'cargo stats' or so. Shouldn't be to hard and cargo is extensible.
$ cargo bloat
Compiling ...
Analyzing target/debug/mdbook
File .text Size Crate Name
0.5% 1.3% 166.4KiB regex <regex::exec::ExecNoSync as regex::re_trait::RegularExpression>::captures_read_at
0.5% 1.3% 165.2KiB idna unicode_normalization::tables::compatibility_fully_decomposed
0.3% 0.7% 95.0KiB ammonia html5ever::tree_builder::TreeBuilder<Handle,Sink>::step
0.3% 0.7% 92.5KiB idna unicode_normalization::tables::canonical_fully_decomposed
0.2% 0.5% 64.6KiB unicase unicase::unicode::map::lookup
0.2% 0.5% 63.4KiB idna unicode_normalization::tables::is_combining_mark
0.2% 0.4% 55.8KiB regex <regex::re_trait::Matches<R> as core::iter::traits::iterator::Iterator>::next
0.2% 0.4% 55.5KiB regex regex::re_unicode::Regex::find_at
0.1% 0.3% 41.7KiB unicode_normalization unicode_normalization::tables::composition_table
0.1% 0.3% 36.2KiB rand_hc rand_hc::hc128::Hc128Core::sixteen_steps
0.1% 0.3% 33.8KiB regex regex::re_unicode::Regex::shortest_match_at
0.1% 0.3% 32.4KiB rand_hc <rand_hc::hc128::Hc128Core as rand_core::block::BlockRngCore>::generate
0.1% 0.2% 31.4KiB ammonia html5ever::tokenizer::Tokenizer<Sink>::step
0.1% 0.2% 24.5KiB idna unicode_normalization::tables::canonical_combining_class
0.1% 0.2% 21.8KiB regex aho_corasick::ahocorasick::AhoCorasick<S>::find
0.1% 0.2% 21.6KiB regex aho_corasick::ahocorasick::AhoCorasick<S>::find
0.1% 0.2% 21.0KiB clap clap::app::parser::Parser::get_matches_with
0.1% 0.2% 20.3KiB ws ws::io::Handler<F>::handle_queue
0.1% 0.1% 19.0KiB ws ws::connection::Connection<H>::read_frames
0.1% 0.1% 18.7KiB env_logger termcolor::Ansi<W>::write_color
35.3% 91.6% 11.4MiB And 58189 smaller methods. Use -n N to show more.
38.6% 100.0% 12.5MiB .text section size, the file size is 32.4MiBA typical smartphone ships with around 10,000 times this much storage capacity and enough RAM to hold it 100 times over.
This is bloat?
I mean, I get it, the binary used to be only 2MB, 1/3rd the size. But are numbers this low really worth worrying about? I think a GUI app in 6MB is hugely impressive.
I genuinely thought he was going to say it was 100MB or something higher.
I don't really see the difference between 5mb and 20mb bins. And I'm a fan of minimalist OS systems like Archlinux with a slim set of running services. Disk space isn't really the main concern besides as a symbolic measure of cruft. No system is ever going to fill up space by having too many OS/terminal programs installed.
But I also don't come from the nostalgic Unixy C programming world. Just a Unix user. So I may be biased.
You are speaking about _latest_ Pis.
Besides that was an example, I have plenty of other IoT and embedded style hardware and they are all fitting this criteria.
Simply put the size of binaries has never been a real concern for some time now and the few rare cases where it might matter probably aren't worth all the effort made obsessing over sub 50mb bin size.
99%+ of it will be people who "feel" like it should be small, without any good reason.
You think that because nobody cares about bloat anymore, so everything else is large.
Win32 isn't actually all that bad and getting simple GUI programs with icons under 10KB is completely doable.
Thinking that GUIs need to be 6 MB or painful is a complete false dichotomy.
It's _worth_ it to trade 1mb, 10mb or 100mb of app size in some cases. That's why you don't hand-craft your "simple 100kb guis" in assembly and have them only be 1kb. Exactly the same principle applies here.
Plus, realistically, users don't care. Not one bit. That's why Slack is out here capturing the market, while some other lightweight and exquisitely coded 10kb tool written in C isn't. Because they are shipping features the users want and iterating fast on their memory hungry and bloated platform, while the other one segfaults when there is an unencountered error.
You can say users don't care about bloat and speed, but when they have an alternative that is clearly not the case. uTorrent destroyed the market share of other torrent clients by being lightning fast and tiny. Chrome captured market share off of being fast. IE originally killed Netscape because it 'loaded' much faster. Winamp won because it was fast and tiny. Google won because it loaded fast and the searches were fast. Google maps won because it was full screen and still faster than MapQuest. People hate the Reddit redesign because it is slow and bloated. People like hacker news' interface because it is fast. People upgrade their phones to see dramatic speed differences. A major advantage of apple is their faster CPUs.
When users have no choice, they put up with whatever bloated nonsense they have to. When they have a choice, they do actually go with interactivity and less latency.
You can be patronizing and pretend that it's archaic to care about well made software that doesn't take up 100x the resources it should need, but when someone wants software that gets out of their way, scales well, or runs on a low power platform, that 300 MB chat client isn't going to cut it.
Also, iteration speed is something else users care about.
I disagree. I develop an audio workstation - https://ossia.io ; the total size is between 50 and 100 megabytes depending on the platforms. It uses Qt and LLVM and is itself around 500kloc so I'm already around the lower limits of what I can do.
My users, & much people on the internet keep comparing it in size to Reaper, another DAW where the binary is around 10 megabytes (https://www.reaper.fm/download.php) - but they wrote their own gui toolkit and language interpreter.
http://blog.johnnovak.net/2016/05/29/cross-platform-gui-trai...
One thing I wondered about Qt is if there's a way to trim out anything an app doesn't use. Have you seen anything like that?
Along with the following shell command :
grep --only-matching --no-filename -R 'include <Q.*>' | cut -f2 -d' ' | sort | uniq
you can quickly see what must stay and what can goWe already could use Turbo Pascal (OWL), Delphi (VCL) and C++ (VCL, MFC, OWL) on Windows 3.1 and deliver applications that could fit on a floppy.
For example, I keep Facebook off my phone, and when I need to download it (for example to rsvp for an event) the app is ~100MB, which means 1> I can’t even do it unless I’m on wifi and 2> it’s slow even there.
I’m having trouble understanding the practical impact of 6MB. Many web pages are larger than that. Even on the Mac I bought in 1994, that would have been a reasonable size for an application (and it only shipped with 120MB hard drive).
But maybe I’m missing something. Some folks have mentioned CPU caches which is interesting — is that the problem?
Windows 3.11 required 4MB of RAM and the whole install took <20MB of disk space, and that's the entire OS with all of its utilities and libraries.
A typical smartphone ships with around 10,000 times this much storage capacity and enough RAM to hold it 100 times over.
The fact that it can hold that much does not make it right to waste resources. To contrast, video or audio is a good use of the space it takes up in general, because there has been and continues to be research in compressing that data, and it's pretty close to being as small as it can practically be. Apps are not a good use of space because we know roughly what the lower limit is --- and the current average is a few orders of magnitude more than that.
Sure, and the moon landing used computers with less processing power than your kids calculator. That doesn't mean we should use those to put people on the moon over faster hardware.
Does the fact that older, slower and smaller hardware and software once existed mean we should spend time, resources and potentially sacrifice features to... what? Hark back to the old days where we had 128kb of memory and hard disks the size of vinyl records?
A 6mb GUI app _is_ impressive for right now. At some point in time it would have been absolutely massive, and the way it's going, at some point in the future it may well be absolutely minuscule. And that's not a bad thing.
In general, complexity is indicative of a poorer design, and the fact that an entire operating system + utilities can be delivered in a comparable size to a gui app is hard to look away from.
The author says, "Once you accept bloat, it’s very hard to claw it back". The fear is that bloat increases at higher rate in proportion to its capability and computing power.
I'm cognizant that I'm very much in the minimalist camp with regard to computers, so I do make sure to peek outside and remind myself that stressing about an executable size is a bit unwarranted. But that being said, we must remain vigilant!
If you want to use windows as an example. Why not take the current version? It uses what like 16GB for a basic install (I wouldn't know, I don't use it)? Compare the 6Mb GUI framework to that.
India, China, Africa. Huge, huge markets. Slow, old, crap hardware and networks. Do the math.
Just go at any iOS or Android conference discussing user monetization.
APK/IPA sizes are the number one reason why consumers uninstall software and the reason behind the ongoing module format efforts from Apple and Google.
I have written GUI apps that could fit in a floppy.
> discussing user monetization
Monetization must be a much bigger reason. A lot more users would angrily uninstall an app that showed them a giant full screen ad.
One recent case we saw a similar tradeoff was the observation that the unicase dep adds 50k to the binary size for pulldown-cmark. In this case, the CommonMark spec demands Unicode case-folding, and without that, it’s no longer complying with the standard. I understand the temptation to cut this corner, but I think having versions out there that are not spec-compliant is a bad thing, especially unfriendly to the majority of people in the world whose native language is other than English.
In the Python world, you can install an "extra" along with a package. So you can make the deliberate decision to omit Unicode case folding from your CommonMark parser. Maybe something like that is possible with a crate?
That said, I think this is a non-feature in the spec, if anything. I see the value in recommending (but not requiring) Unicode normalization, but I don't see the added value of Unicode-aware case-insensitivity. Maybe it's more important in non-Latin text.
I believe that -- to give one example -- this is something that Chrome implements, but Firefox does not. It's a huge pain to have to match accents perfectly in a text search on a page when you often want your search to be accent and case insensitive. This sort of thing is very important for any text search workload, I imagine.
The article mentions a few places that use cargo "features" for exactly that sort of thing.
It's a complex trade-off of runtime performance, code clarity, productivity, safety, binary size, and compile times. Rust tries hard to eat all the cakes and have them too, but it can't do miracles.
And looks like the author is on the right track to tackling the problem — moving less common functionality behind feature flags, pregenerating data "offline", profiling and trimming code with the help of cargo-bloat.
This is just the reality of engineering -- there are no silver bullets.
Sure, it'd be nice if any language could give you the space efficiency of dynamic dispatch with the runtime efficiency of monomorphized generics, but those two things are fundamentally in tension. Neither Rust nor any other language can fix that.
Rust at least gives you a fairly easy choice of which you want in any given circumstance. Most languages just pick one or the other universally.
I am wondering if Rust has a project of shipping Rust compiler embedded in your executable, so that one could compile sym into code.
Also not a silver bullet, because there's non-zero costs in memory, CPU overhead, I$, etc to the statistics gathering, stop-and-jit-the-dynamic-call-and-change-the-call-sites and so forth.
In long running server processes this can mostly amortize out nicely over a very long run, but for interactive applications it can add noticeable lag.
> In the Python world, you can install an "extra" along with a package. So you can make the deliberate decision to omit Unicode case folding from your CommonMark parser. Maybe something like that is possible with a crate?
This is definitely possible in Rust with Cargo crate features! [0]For instance in a game library like quicksilver you can opt in to WebAssembly support, images, fonts, audio handling, etc. Then the image or audio library themselves can put formats and codecs behind feature flags for instance. Each crate then uses conditional compilation to branch out these features at compilation time. More commonly as an end-user, test modules use a special conditional compilation macro [1].
[0] https://doc.rust-lang.org/cargo/reference/manifest.html#the-... [1] https://doc.rust-lang.org/stable/rust-by-example/testing/uni...
I don't really see this recommended anywhere, especially re: polymorphism. Async _maybe_, but it's pretty situational.
This seems like a specific thing that would be a measurable win and a not uncommon problem (e.g., I have a test kernel module that doesn't have many dependencies, and it still has two generic-arrays, two proc-macro2s, and two unicode-xids). Is there something that could be done here technically, such as pull requests to bump common crates to using the same version of even-more-common dependencies?
This is up to 0.13.2... plenty of supposedly breaking API churn, no wonder you have two copies.
Yes, your dependencies probably rely on different 0.x versions, and a pull request could fix that. You can inspect your Cargo.lock file to figure out which ones are to blame.
> Is there something that could be done here technically, such as pull requests to bump common crates to using the same version of even-more-common dependencies?
Yep, that's the fix.
README.md badges like https://deps.rs/repo/github/rust-lang/cargo can alert people to out-of-date dependencies.
You can even automate some of the pull requests with something like https://github.com/marketplace/dependabot-preview if you control the repositories.
Suppose it cuts build time from 5 minutes to 30 seconds....
Technically, a transparently mirrored file system, a strong compiler cluster (memory, cores, etc). And some predictive ML. But you end up with a binary-equivalent (verifiable) output.
Any thoughts ?
Not sure how the buisness model for a cloud compiler would work, but I would be interested.
While you could assume any maliciousness or security compromise would be caught, as you can see from the rubygems news today this is not instant and it adds another point of failure.
It's distro maintainers' job to test packaged apps and converge on a known-good version of a shared lib. This is only a problem if your distro doesn't package the apps you need, and you build from upstream source ignoring the distro and using all the random versions upstream happened to test.
flatbuffers seems to be the go-to serialization lib, in terms of speed and mem efficiency:
You can have the same issue if programming in any other language, including C: excessive indirections, inefficient algorithms, bad abstractions, excessive use of unnecessary libraries, etc.
It's indeed easier to "bloat" your resulting binary in C++ or Rust given the easiness to do higher-level abstractions; since you can more easily program complex solutions you also need to consider your design and the trade-offs on your code.
I'd also like to point out that, in comparison:
* Rust bloat is a speck compared to hundreds of megabytes for a similar Python program + runtime including the same amount of code.
* Rust bloat can be mostly optimized away for systems that really care about excess/unused code - you have #![no_std], disabling of backtraces, aborting on panic, and a lot of other optimizations that throw away a big portion of extra functionality not needed for things like embedded. You have little alternative on things like Go besides removing debugging symbols and doing tricks like "dynamic decompression" (which could also be applied to Rust programs to further reduce their size, btw).
Bottom line is: Rust makes it easier for you to "just add a new library" and it also make you more mindful of the bloat, but we need to keep it in perspective.
I have myself made another localization library that use a simpler model and compiles quite fast when using the rust `gettext` backend: https://github.com/woboq/tr
It doesn't make sense. Either your complete code base is async or it is not. If you have a single blocking code point in it. It is not async anymore, since it can't handle any other schedule async tasks while waiting for the blocking call.
> Once you accept bloat, it’s very hard to claw it back. If your project has multi-minute compiles, people won’t even notice a 10s regression in compile time. Then these pile up, and it gets harder and harder to motivate the work to reduce bloat, because each second gained in compile time becomes such a small fraction of the total.
This particular problem can be addressed head-on, I think. It would seem feasible to have the compiler to distinguish between the target application (library) and the dependencies. Then it could report the compile times as separate values. This could be built into CI/CD tools as a way to catch application level compile time changes.
Of course, this approach wouldn’t tell the entire story, but it would likely serve as a canary in the coal mine at least.
What about if rust could support something like dynamic Linking/loading. Like have some crates globally installed and while building we could link to the global one instead of locally getting all the crates. Like C/C++does it right?
The fix is simple, when it happens: Patch A/B to use the latest major version of C, fixing the source code as necessary. You can [patch] locally until upstream accepts your Pull Request - which might include a https://deps.rs/repo/github/rust-lang/cargo badge in their README.md, to encourage them to continue to keep C up-to-date.
:)
Maybe I have a hard time adapting myself to rust? Maybe my brain is too much "wired" to a C style syntax. It still seems that to me, C-style syntax is just better.
I would rather prefer a C++-like language that breaks down from backward compatibility with C while keeping its simplicity, has STL containers, and is simpler to read and use.
Rust is cool, but I'm just curious if it can really be adopted for large project to justify rewriting existing code.
Anyway, what is "not simple" about Rust's syntax?
; can also make a lot of difference, which is a little weird.
I don't understand why there is both str and String, seems complicated for nothing.
Option things are not really clear yet, and I don't understand what is their use, it seems like an alternative to unions, but few developers use unions anyway...
Pattern matching seems powerful but I fail to understand its usage, and I'm a little skeptical about the machine code it generates.
D is fine, but I think that even D is not simple enough.
Rust seems like it's awesome, but it requires to entirely rethink how you write code, and I've never been a fan of high level abstraction.
`String` is roughly a Java `StringBuffer`, `&str` is roughly a C++ `string_view`.
Yes, you need to rethink how you write code. Rust relies a lot on the basics of typed functional programming. Reading a Haskell tutorial up until the M word would help a lot. But surely there's a lot of good guides written for Rust specifically.
tl;dr the core concept you're looking for is sum types, aka discriminated/tagged unions. If you're going "up" from C level abstraction, imagine a C union starting with an enum that indicates which value is there. That's essentially what it is at the memory level (modulo optimizations). But conceptually, you just have a "this OR that" type. Pattern matching is how you access values of that kind of type.
https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html