Safety in the systems space is Rust’s raison d'être
Which, ironically, is the place it most falls short. Bust-on-OOM and macro-hell just for a global, doesn’t make for a good “systems space” language.
To clarify:
libcore, which is a subset of the standard library that doesn’t allocate.unsafe. You might be thinking of the lazy_static! macro, which essentially provides a safe convenience for running one-time synchronized initialization code to produce a value.Rust’s standard library is part of Rust. I think it should be simply admitted that Rust made different tradeoff than C here, and in a sense Rust is less safe than C with regard to OOM safety. I happen to think it was the right tradeoff, but it does make Rust less suitable if you want OOM safety.
Handling OOM in a complex application is so difficult that I think it’s more honest to make the default be a panic–that’s what most of the complex C code bases I’ve worked in do, either explicitly or (ugh) by missing enough null checks that they’re going to fault quickly anyway. It is often a better strategy to just panic and let the supervisor start over than to attempt to actually handle every possible OOM in a stable way.
Yes, but it’s important to point out that there’s an opt out strategy available. It definitely won’t make everyone happy of course right now, but it’s there! :-)
Out of curiosity: if you hit an OOM is there much you can reasonably do? It seems like if you had things you could free you would have already freed them?
Big and complex applications often have lots of freeable data floating around for a purpose. Like, look at your browser. It caches tons of stuff for performance. But as soon as that memory is really needed for something, they can just drop it.
Games, mapping applications, image editors, etcetra tend to have lots of data and assets that are worth keeping loaded in memory if there’s enough if it, but can always be reloaded if needed.
Another example would be a chat client that keeps logs. It’s a nice feature, but it’s not cool to crash when an alloc failed because there’s a gigabyte of history.
Some of these applications often self-regulate by imposing some arbitrary limit on the caches, history, etc. But it is more about being nice (not consuming too many resources, for arbitrary definition of too many) and optimistic (“if we only use this much, we probably won’t run out of memory.. on a system that doesn’t run a lot of other stuff”) than it is a solution.
Some applications cache data to the disk to conserve memory. It’s a shame we don’t have good apis for having a little discussion about memory use with the system; it would be nice if we could let applications either allow the system to swap data out for them, or let them voluntarily go of that data to make room for others.
I don’t know why people keep perpetuating the myth that there’s practically nothing you can do about OOM. If anything, I think dealing with memory exhaustion should be more important today than ever before as applications have grown in complexity and often can slurp as much RAM as they are given while people run on them multitasking systems with more than order-of-magnitude differences on the available memory. The said systems are often busy with a multitude of such memory hungry applications simultaneously.
Ideally, if an application that really can’t recover from OOM by freeing stuff of its own, the system could ask the browser and the games and other stuff to make some room, which should amount to the same procedure that they’d run themselves when an alloc of their own fails.
The solutions we use now are pretty spartan and frankly shit. Linux with overcommit grabs the OOM killer and kills random shit, which is absurd, except maybe as a last resort after other, better solutions have been exhausted. Other systems actually let allocs fail, and may employ rlimits to constrain the processes and hopefully leave enough breathing room for everyone. It might or might not help. But it’s hard to make rlimits right when an application can use anywhere between 50 megabytes and a dozen gigabytes, on a system that might have 512MB or 32GB. It helps even less when so many applications are written with the prevailing mentality from GNU/Linux land. What hurts me most is that people keep perpetuating the myth and discouraging people from actually caring about memory exhaustion and recovery.
A “please collect your garbage now” signal (with a size hint?) could be a good start.
It would be nice if we could let applications either allow the system to swap data out for them, or let them voluntarily go of that data to make room for others.
This was the idea behind POSIX_FADV_VOLATILE. Alas, as far as I know it went nowhere.
This article is maybe a bit more skeptical of Go’s GC tradeoffs than is warranted. Like Compaction – yes, Go trades off a compact heap for shorter pauses. But that’s maybe not a bad thing – we’re in a 64-bit world and for a typical app with a smallish working set it’s going to be a long, long time before you fragment 64-bit virtual address space enough that you’ll OOM.
There’s certainly applications where compaction matters, but they’re a lot less prevalent than they were in the 32-bit era and it feels like we never adjusted our concern about the magnitude of the problem accordingly – we just kept assuming compacting collectors made sense in the average case and never revisited the sanity of that assumption.
long time before you fragment 64-bit virtual address space
No one is concerned about the address space here. Fragmentation wastes physical memory.
There’s certainly applications where compaction matters, but they’re a lot less prevalent than they were in the 32-bit era
Compaction is no less useful in a 64-bit environment.
No one is concerned about the address space here. Fragmentation wastes physical memory.
I’m extremely skeptical that this is a problem most apps would encounter. There are pathological cases in which you could, say, keep one byte per page alive and keep more than physical memory’s worth of those pages in your working set, but you’d really have to be working at it to get to that point. Your average Go deployment just doesn’t have that big a working heap – and isn’t doing heap allocations that granular. Optimizing for the corner case doesn’t make sense.
People downvoting this to zero might consider asking themselves how it is that C and C++ programs, or Python and Ruby servers with their heavy heap usage and 100s of megabytes of short-lived garbage generated per-request, nevertheless somehow manage to survive in production with months or more of uptime without being crippled by the dread spectre of physical memory fragmentation due to their total lack of memory compaction.
C++ avoids memory fragmentation by using smarter allocators that reuse the same address segments whenever possible.
Python and Ruby use malloc, one of the aforementioned smarter allocators.
Indeed.
Now, the $1,000,000 question: does Go, the thing we’re discussing here, use a malloc-style free-list allocation system, or does Go use a bump pointer?
C++ and C programs don’t generally make heavy use of a GC, so they don’t need a compacting GC.
The uptime of Ruby and Python applications tends not to be months, at least not in my experience. Using Eye or Monit to kill the app and restart it when it exceeds a few hundred megs is very common.
I don’t know if I’d call that ‘common’ (I’ve been doing a lot of Ruby for the past 8 years). Your average Rails application boots at a few hundred megabytes and tends to stay roughly in that range, unless you’ve got a leak (accidentally retained ‘dead’ memory, or unreclaimable memory like strings you’re turning into symbols on a frequent basis) – then, people in a hurry tend to start killing it and rebooting rather than hunting down the real issue. But that’s not really related to any kind of physical memory wastage, and you see that behaviour even in the presence of compacting collectors (we reboot a lucene instance once a week because it’s easier than hunting down its leaks).
I’ve never seen that done in production for the issue “lobstersinabucket” was talking about – everything that should be GC’d getting GC’d, but in such a pattern that sparsely populated pages of memory leave a lot of physical memory wasted, and the reason for that is, as peter pointed out in the other reply, that Ruby’s allocator re-uses free space in pages where it can.
Go’s allocator also does this, which is why the idea that without a compacting GC a significant proportion of physical memory will end up wasted on holes in virtual memory pages is unlikely in most workloads.
I don’t know if I would call that common…
Well, here we disagree.
Would you say it is common to run Ruby and Python web applications under load – not heavy load, just the moderate load of a site like Airbnb – without restarting them for months at a time?
I would say that most (like 99%) of Rails installations are not experiencing the, uh, “moderate” load of a site like Airbnb
edit: anyway, that’s kind of tangential to the point. Even in high-traffic installations where nodes restart often, it’s not generally swiss-cheesed vm pages choking out physical memory that motivates the restarts.
I would say that most (like 99%) of Rails installations are not experiencing the, uh, “moderate” load of a site like Airbnb
Although it may seem high, practically speaking you get only tens of requests per second per app node at a site like that. It’s not like your doing 1000 QPS per server.
anyway, that’s kind of tangential to the point.
Well, it was something that you offered as support for your idea – and “falsity proves anything”. The original article raised concerns about Go advocates and their tendency towards exaggeration with regards to this issue and you would seem to be sadly on trend.
[Comment removed by author]
Yes and no. If you’ve got cache-locality sensitive code, you should really be taking steps to ensure that that data is being allocated in a cache-friendly way to begin with, not allocating it willy-nilly and then hoping the GC pushes it all nicely together for you. Compacting can help you improve locality only in places where you got your allocation patterns wrong to begin with.
[Comment removed by author]
Your lecture is besides the point.
I’m…not lecturing you? Just trying to have a conversation, here.
Thing is, all code is cache-locality sensitive. A compacting GC can improve cache locality for all code automatically. Your argument that an automatic, compacting GC is not desirable because you can manage memory manually makes no sense.
I’m not talking about managing memory manually – I’m talking about caring about your allocation patterns and how they impact your performance. You have to do that whether or not you have a Garbage Collector at hand.
Consider Minecraft – they’re not managing their memory manually, but they absolutely care about cache-locality and allocating data in a cache-friendly manner. They’re definitely not just leaving that to the whim of the GC. Once you’ve got your major hotspots allocated in a cache-friendly manner, any further wins coming from the compactor are a) completely unpredictable between runs (you’re literally relying on blind luck) and b) swamped by other concerns anyway. Considering that compaction isn’t free, that’s not a compelling argument for it imo.
It’s almost free to implement, since LLVM supports them natively, and it’s sometimes useful for things like implementing bigints. GCC and Clang already support them via __int128, why not Rust?
It’s almost free to implement, since LLVM supports them natively,
LLVM also natively supports 256 bit integers, why aren’t they being added to Rust as well?
and it’s sometimes useful for things like implementing bigints.
How? Bigints are typically implemented with words.
GCC and Clang already support them via __int128, why not Rust?
I didn’t think Rust’s design philosophy was “C compilers do it, why not us?” ;-)
A small handful I disagree with:
And a few that bear reinforcement:
So, one part with your integer division bit: this really depends if 2 as a literal has to be an integer or could be a float. With Rust, we force you to write 2.0 to get a floating point literal, and every once in a while, someone gets Real Mad that they have to type the extra. (Technically you can even write 2., but I don’t like that at all.)
Dealing with floating point numbers in general can lead to people getting Real Mad.
Is this historical OCaml influence? I’ve seen people upset there about having to opt-in to a lossy numeric representation, but I appreciate needing to be explicit.
Could be worse. Could be they have to write 2 for an int, 2.0 for a double, and 2.0f for a float. Inconveniences abound.
Integer division is clearly useful, and I don’t think that it’s unreasonable to have / represent integer division on int. In fact, it’s a valuable first lesson: the meaning of / is different on int versus float.
My complaint would be how negatives are handled. I would prefer that % (mod) return a positive representative at all times and / (div) round down, e.g.
-1 / 7 = -1
-1 % 7 = 6
You still have the mod/div law, d * (n / d) + (n % d) == n, but you also have n / d == 1 + (n - d) % d, which is broken with the round-toward-zero behavior. I don’t see what the value in the round-toward-zero behavior is.
Assignment-as-expression is awful for modern code. It makes code slightly more concise but a lot harder to read, and far more error prone. It also lets the if (x = 2) mistake, which ought to be a compiler error, get through. It’s probably a remnant of a time when storage was so expensive that even source code storage needed to be considered, but these days its value is negative.
I say this because, in 2016, you pretty only use C for a certain type of highly careful system programming where you want to know exactly what is going on (and can’t afford, say, the nondeterminism that comes with GC and a powerful runtime). In this case, you want code to be verbose and boring, not clever and succinct but harder to follow.
It also lets the if (x = 2) mistake, which ought to be a compiler error, get through.
FWIW, Clang and GCC both emit a warning for this, with -Wparentheses, which is also enabled by -Wall. To silence the warning, you can write it more explicitly as if ((x = 2)) instead.
[Comment removed by author]
This seems like an odd rationale. Even the smallest C compiler should be smart enough to use such opcodes when faced with a += 1 or -= 1.
Blaming the programmer: This is not just a problem with C, either. It seems a lot of programming languages respond to common error cases by telling the programmer to be more disciplined, even though the fact those cases are common indicate this doesn’t work. One of the things I like about Rust is that the language developers specifically design in favor of automatic safety over requiring “discipline”.
And still, Rust has quite some “you need to know that”-corners.
It can’t be avoided, but a “blaming the programmer” stance doesn’t help - we need to promote those flaws together with the strengths.
[Comment removed by author]
That’s not what the commenter said. It even comes off as a deliberate strawman to prop up subverted solutions in Russia. The commenter indicated it was a closed-source OS in a country with highly-subversive, intelligence organizations constantly pulling shady shit. That’s a good reason to avoid it. Likewise, a closed-source product in a U.S. surveillance state (i.e. Windows) should be avoided for same reason. The logical conclusion is to avoid closed-source products in countries or from companies prone to subversion. Another would be using FOSS products that get a lot of scrutiny esp by people in diverse nationalities.
That’s what U.S. (Red Hat), Germany (SUSE), France (Mandriva), Russia (Astra), China (Kylin), and North Korea (Red Star) did in subsets that were concerned about proprietary software for various reasons or wanted FOSS’s extra benefits.
I note that the Russian government has expressed interest in ReactOS, and funded developers. Russia also has an indigenous CPU architecture too, that’s VLIW and good at emulating x86, like a Transmeta CPU. (In fact, there’s a link between Transmeta and MCST - see if you can find it…)
“has expressed interest in ReactOS”
They’ve also seen the Windows source code. This could be one of the few times where their espionage schemes could help us all out if they did it carefully. Use Windows code to spot all the undefined behavior and weird stuff then clean-slate solutions in the ReactOS code. I doubt they will but had to share an amusing possibility.
“here’s a link between Transmeta and MCST”
I’ll be damned: the floating point and testing rig for Transmeta done at MCST. Those people keep impressing me. Although aware of Elbrus, I didn’t know the recent one had x86 emulation. Thanks for the tip since it’s possibly useful anti-subversion schemes! :)
IIRC, Boris Babaian worked for Transmeta, possibly others.
There’s also links between Elbrus and Itanium - the software emulation for x86 Intel licenses was developed by the same people who did Elbrus', and I believe Transmeta’s as well. VLIW is incestuous!
I do remember reading an article where they a Russian firm made a better Itanium than Itanium or something that Intel bought either for the advance or in self-defense. I can’t find the article right now since the Elbrus 4 results are piling up everywhere. It was funny, though. Aside from using Leon or something, I also tell people wanting a FOSS processor with high-performance (eg Raptor crowd) to just raise money to pay a top-notch design house to straight-up build one. At least the pipeline, memory subsystem, and NoC so CPU-bound stuff would be fast. I’d say whatever company was one-upping Intel on VLIW with relatively few staff is a good candidate.
I’d be wary of closed source OSs from Russia, China or America or Britain. They all have governments or agencies that can/do compel companies to introduce backdoors or otherwise spy on their users.
It’s fascinating that he was smart enough to discover an unsecured obscure DNS feature, find a public-writeable web server, and then island hop in to their network, yet somehow dumb enough to think they would give him a job rather than trap him.
The author doesn’t seem to know what a debugger is…
Debuggers are still good at debugging serial code, but these days my code is asynchronous and distributed over many hosts. There is no concept of “stepping through code” in asynchronous systems - stepping implies that you are on a single thread, running on a single machine.
If all debuggers were good for was setting breakpoints, his headline might not be stupid.
I had to laugh (in a kind way) at this slide:
https://talks.golang.org/2016/applicative.slide#13
We have one in the Rust world, too, just the axes are different (Control vs. Safety):
It is interesting that the golang axes attempt to be accurate, where as the the Rust one is just the obligatory up and to the right (which may also be accurate, of course!).
People in my very unscientific survey suggest that Go:
Benchmarks suggest that Go is:
i’m not a huge fan of go viewed purely as a language, but if you consider “fast and fun for humans” as a sum of language + dev tooling + deployment, they’re selling themselves short on that axis, if anything.
3. is productive
To a point. There is a lot of copying and pasting, a lot of “verbosity” for the sake of … not sure what, but the don't DRY out quote from one of the posted slides sort of explains it.
I like go for small projects, for large projects with lots of code it becomes an unrefactorable mess.
Copy and paste can be productive. Go lacks a lot of the generalized abstractions that make it a good language, but in my experience it’s as fast, or faster to write in Go vs JavaScript or Python.
I don’t defend Go because I like it, mind you. But, I can’t ignore the speed in which I and others have developed pretty high quality, and scalable stuff with it, either.
I’m somewhat surprised by the existence of large projects written in go.
The language is <10 years old - either someone wrote a lot of code in a very short period of time, or we have different definitions of large (my first job after uni had >1million SLOC and ‘unrefactorable mess’ did not begin to cover it).
Kubernetes is in that range but i may have counted dependencies.
edit: 700k lines without dependencies, 1.2 million with them
There’s no clear definition of “large”. 1 mloc might be considered “very large”.
https://en.m.wikipedia.org/wiki/Programming_in_the_large_and_programming_in_the_small
Go offers several ways to avoid repeating code. Each has its own tradeoffs. Sometimes copy and paste is the most appropriate, but its hardly the default.
Re: large systems. I’m just getting there with Go and to remain effective I’ve rewritten a few packages multiple times. I see no evidence unrefactorable messes are inevitable.
Our startup switched to Go almost a year ago and we came to the same conclusions.
Can confirm that 4. is indeed very true, we were surprised by how easy it can be to output scripts that are still maintainable/readable and being written on a short timeframe.
Regarding 1. and 3. our feeling is that it clearly depends on what is being implemented.
As soon as the project requires a lot of business logic, in our experience, expressivity became clearly a problem. We ended with a lot of boiler plate and some rather boring code but it still executes pretty fast so it’s a fair tradeoff. Maybe with more experience this will come better, we’ll see. If anyone could point me toward resources addressing this, that’d be awesome ! :)
On the other hand, when it comes to writing stuff that needs to be fast and mostly does one critical thing, the results are speaking for themselves, code is simple and concise and pretty damn fast. It’s not that we couldn’t have written it in C, it’s just that it’s really comfortable to do it in Go and quickly yield clearly solid results (for us).
Still our team feel some frustrations regarding the lack of abstractions, but we’re solving this from now on by using Go mostly in the latter scenario.
There’s also Simon Peyton Jones' chart, where the axes are “usefulness” and “safety”.
<3. I love how this is obviously from the time where STM seemed to be the topic of the day.
Also: I have a horrible, horrible memory for faces. Is that Erik Meijer in a shirt with just two colors?
Also (II): Interesting that the idea of bringing SQL stuff back into the programming languages has worked out.
This is a favorite video of mine, for a couple of reasons:
As a real world example of #2, I presented at the same conference as this presenter, and he came up before mine to introduce himself, say hi, and ask if we shouldn’t grab some lunch sometime since we both live in NYC. I was very humbled. I wouldn’t say the Rust and Go teams know each other super well, but we’re extremely friendly and respectful of each other’s work, generally speaking.
This slide looks like it was written by someone who doesn’t even know C++. They put Go way above it, but put C only barely below it.
This is completely wrong; it should be the other way around. Go is only marginally easier than C++, while C++ is much easier than plain C.
You are aware that this is a slide of a rust core member? They probably know C and C++ quite well.
All slides are marketing material and the grouping isn’t scientific.
C++ is much easier to write than C is but not necessarily to read (other’s people’s code). Because of how verbose Go is, it is much easier to read other people’s code.
There’s another important reason, that the article doesn’t mention. In spite of exceptions not being necessary, they are still used in release mode. And LLVM uses “zero-cost” exceptions, which are rather bloaty.
Are you speaking about Rust or C++ here?
While currently, in Rust, landing pads and such are included, in nightly you can compile with an abort instead. That said, it wasn’t in the article.
Uh, since when is it acceptable for memmove to simply copy in the opposite direction? My understanding is that it must work no matter which part of the data is overlapping, as if it made a temporary copy somewhere else entirely. Simply reversing the copy direction cannot guarantee this… memmove(&s[1], &s[3], 3);
I think he oversimplified a bit there. You at any rate sometimes need to copy downwards, and sometimes need to copy upwards, so having the direction flag smashed by signals kills you either way, but I agree that his comment as-phrased seems very weird.
simplified is
That would be memcpy (although it also makes no guarantee on the order/direction)
What’s your point? memmove() is just an overlap-safe memcpy(), trivially implemented with variable direction and atomic width.
Wouldn’t copying in reverse work just fine for that? Copy 3 to 5, then 2 to 4, and finally 1 to 3?
EDIT: Whoops, mixed up the source and dest.