21 ms·
Being fair about memory safety and performance
- fulafel 5y agoThere's a presumption here that checked access would cost some nr of nanoseconds per access, but this often isn't the case since predicted, not-taken branches tend to have 0 cycle latency in recent CPUs.
- Jensson 5y agoDepends, if the function is vectorizable then the cpu can do more elements at a time if it doesn't do the branch prediction work. It is true for non-vectorizable work.
- fulafel 5y agoIn autovectorized loops, the generated code typically needs length checks (or static length proofs) to handle tails of vectors. But yes there are still cases where the cost can be measurable.
- celeritascelery 5y ago> predicted, not-taken branches tend to have 0 cycle latency in recent CPUs. This is not the case. Due to instruction level parallelism, the throughput could be unaffected, but you will always have latency penalty. The CPU still needs to run the check (access the length and compare it to the index) and this adds latency. On top of that, it also increases code size, which can impact the instruction cache and binary size. It’s a small penalty, but it’s not 0.
- fulafel 5y agoSpeculative execution enables continuing along the predicted branch without stopping. You do need to have the ~2 instructions to get the length test input on hand but that usually can be eaten by insn level parallelism without hurting the latency of the array operation.
- errantmind 5y agoDo you have any evidence of this claim? Perhaps a benchmark? This doesn't align with any of my performance optimization experience.
- fulafel 5y agoI went looking, and seems I have to walk my claim back somewhat. Wide issue OoO processors hide a lot of the overhead but not all of it.
- SiebenHeaven 5y agoGreat article, I'm bookmarking it so I can point to it if and when required ;p
- habibur 5y agoRead only top half of the article. Would like to add that at least in plain C, doing var[index] doesn't invoke any checked() or unchecked() access function call. It's rather compiled into assembly instructions to calculate the address where the data is expected and load it into memory in one or two lines.
- masklinn 5y agoWhich is, incidentally, why you can also write `index[var]`. `[]` is really just a convenience for `*(var + index)`. In fact that's exactly how the standard defines it (or at least defined it as of C11, I've not checked more recent versions): > A postfix expression followed by an expression in square brackets [] is a subscripted designation of an element of an array object. The definition of the subscript operator [] is that E1[E2] is identical to (*((E1)+(E2))).
- saagarjha 5y agoYou’re taking C extremely literally, and compilers are not required to do this (for example, if the array is being iterated sequentially, multiple loads may be done at once). Similarly, in Rust a function call is not necessarily going to actually compile to a function call: it is expected that the access compiles down to something very close to what C would do. The syntax is an abstraction, rather than a normative designation to the compiler for how it should generate code.
- RedPanda250 5y ago> Most specifically, gcc and icc are much better compilers for those use cases – empirically – than is LLVM. This is interesting. Where can I read more about this ?
- mlindner 5y agoReally good article that covers some poor arguments about C++.
- the8472 5y agoSince this is about array indexing, if you do slice.iter.for_each() instead of for-in, while-let or manual indexing then it already uses unchecked access under the hood today because internal iteration can use a counted loop and knows it doesn't need those bounds checks.
- vlovich123 5y agoThat’s discussed in the slices section of the article
- snicker7 5y agoAs for array index checking, Julia gets it right. It uses a pair of macros (@boundscheck and @inbounds), which empowers the user elide bounds checking. Moreover, it is extensible to beyond just arrays. I’ve used these macros in my own hand-rolled implementation of a ring buffer.
- lordnacho 5y agoTotally agree with this article, as a fellow HFT coder. The problem in c++ is the surface where you might cause a memory problem is huge. Once it's there, it's a lot of work to test the hypotheses about where it is hiding. On top of that, these kinds of issues can escape your instrumentation in a way that other bugs tend not to. Add some debug lines, things get accessed differently -> Heisenbug. Mega pain in the ass to figure out, lots of time taking everything apart, sprinkling debug lines, running long tests to catch that one time it goes wrong in a million, and so on. He's also right that the array access thing is not a huge thing, it can't possibly be what your decision turns on, and that most of the code doesn't have a tradeoff in performance, because it's in the config stage rather than the hot path. Personally I've had a great time with Rust, it's far more productive than other typed languages I've used. On a business level, the issue with the type of bug mentioned above is it destroys your schedule. I've spent entire weeks looking at that kind of thing, when I was expecting to be moving on with other parts of my project. With my current Rust stuff, I'm doing what I expect to be doing: addressing some issue that will soon be fixed like adjusting some component to fit a new spec.
- pron 5y agoI'm not quite sure who this article is aimed at, and who those "C++ apologists" are, but as someone who programs in C++ all day, doesn't like it at all, and yet won't advocate to switch to Rust, these kinds of arguments are unconvincing. I'm not advocating to switch to Rust not because I don't think it's better than C++. I am absolutely, 100% convinced that Rust is technically better than C++ in most possible ways — in some, significantly better — and worse in almost no way. The problem is that, overall, it's not better enough. Reevaluating a low-level programming language is something that's done in a large organisation or project once every 15-25 years or so. Switching such a programming language incurs a high cost and a high risk, and is a long-term commitment. To make such a switch, the new language obviously has to be better, but that's not enough. It has to be a hell of a lot better (and, if not, at least return the investment with a profit quickly). For some, Rust is better enough. For me, not nearly so. Even though it offers a fascinating and, I think, ingenious path to better safety, it shares some of C++'s greatest downside for me, which is that they are both extremely complex languages. Maybe Rust is simpler, but not enough. Rust also shares what I think is C++'s original misguided sin, which is the attempt to create a low level language, whose code appears high-level on the page by means of a lot of implicitness. I've become convinced that that's a very, very bad idea. If there were no other ideas on the horizon or if Rust seemed like a surefire success, it might have been justified to make such a switch, but that's not the case. Rust's low adoption rate in professional settings is not reassuring to "PL-cautious" people like me, and a language like Zig shows that there are other approaches that appeal to me more, and while more revolutionary and ambitious than Rust in its departure from C++'s philosophy, I think it also has the potential to be better enough. Maybe it will make it, and maybe it inspires some other language that will, or maybe other ideas will turn up. Given the risk and commitment, to me it makes sense to wait. I don't like C++; I believe Rust is better. But that's not enough.
- skyde 5y agodo you think “C” share the same that same miss guided sin?
- pron 5y agoNo, there is very little implicitness in C. However, C suffers from other serious problems that Rust and Zig do address (and C++, too, to a lesser degree). It is extremely unsafe (more so than C++), and has a lower abstraction ability even compared to those other low-level languages.
- sundarurfriend 5y agoThe substance of the article is overshadowed by its unfortunate tone. > But wait! The C++ apologists are still talking! What are they saying? How have they not been completely flummoxed? is just one small sample. I came out of the article liking Rust a little bit less than when I went in (irrational, I know, but true). The quote from The Big Lebowski comes to mind: you're not wrong, author, ...
- patrick451 5y agoYup. The rust evangelists have completely turned me off from rust.
- saagarjha 5y agoThis article has a core point which is good: “in Rust the default is safe, and you have to opt-in to unsafety, but in C++ the default is unsafe, and you have to opt-in to safety”. I think it’s easy to argue for Rust using this construction, because, well, that’s the entire point why Rust was created. But, it really doesn’t take a very long post to talk about this. The remainder goes off the rails, talking about “C++ apologists” (hint: if you’re being “fair”, pick words that are unlikely to cause people to be preemptively upset. This is not one of those words) and their stupid opinions. And the author just trashes them as being complete idiots, but it’s obvious that the arguments come from inexperience or strawmen, which just makes the overall thing not particularly convincing. Saying that the various UB finding tools were useless because you tried using them and didn’t get good results is stupid. Being smug about “people who use modern C++ clearly can’t do HFT, which is the thing that you said you were using C++ to do” is also insipid, just because you spotted the use of a shared_ptr somewhere and read how it’s not zero-cost. Modern C++ has other things in it, you know, many of which are zero-cost and significantly (but not entirely) safer; picking one thing and misrepresenting it does not make for a good refutation. Anyways, coming from someone who writes a lot of C++ and would also like a lot of code to be migrated to Rust for good reasons, it’s a good idea to approach the tradeoffs honestly and without disdain for those who aren’t convinced yet. The core argument I mentioned above and the closing part of the article does do this…but there’s a lot in the middle that doesn’t, and it drags down the usefulness of the post.
- _vvhw 5y ago> This article has a core point which is good: “in Rust the default is safe, and you have to opt-in to unsafety, but in C++ the default is unsafe, and you have to opt-in to safety”. On the subject of safe defaults, just to correct that Rust does not in fact have as much default memory safety with regards to buffer bleeds (e.g. variants of OpenSSL's Heartbleed) as it could [1], because it has unchecked arithmetic (integer wraparound) as the default for performance, with checked arithmetic only as an opt-in for safety. In other words, if an attacker can get some bounds merely to underflow (as opposed to overflow) then they can still read the sensitive memory of a Rust program, even without a UAF or buffer overflow. Bleed vulnerabilities like these are also low-hanging fruit and significantly easier to exploit. In other words, bounds checking only ensures you are within the buffer, but checked arithmetic is still needed to ensure that your index was correctly calculated in the first place. I believe that Rust would be much safer against memory bleeds, if it had checked arithmetic enabled by default for safety, with an opt-out at the block scope level for performance, like Zig has. [1] https://news.ycombinator.com/item?id=29991439 https://news.ycombinator.com/item?id=29991439
- pyjarrett 5y ago> I learned that the issue was in framework code – code written by my boss’s boss. The code was untested, and written extremely poorly, and had rotted, so that it didn’t work at all. One unaddressed issue in Rust is that this could easily happen with a crate, and how hard diagnosing it could be, especially due to implicit behavior via procedural and attribute macros. Also, just because your code is safe, there could still be an unsafe block at the end of any long safe call chain. I haven't been able to reconcile to myself how this isn't just an illusion of overall safety.
- pornel 5y agoPeople complain about Rust using so many small crates, but it's actually its strength: those small focused single-purpose crates are easier to review, test, and fuzz. You don't need to answer a question of "is my 1-million-line codebase safe?", but rather "does this 10-line function uphold Rust's invariants?". It may be a tricky question, but you can focus on it in isolation. The contract between crates is safe, so once you've proven the dependency upholds the contract, you can rely on all its usages being safe.
- cylon13 5y agoI don’t think it’s so much an illusion as it is just not quite as absolute as one might initially think. If you get a segfault in a rust program you know where to look.
- adgjlsfhk1 5y agothe are 2 answers here. the first is that the crate is less likely than a c++ library to have this bug since stuff is safe by default. the second is that having a smaller set of unsafe stuff in your code base makes it easier to rule out your code as the problem.
- oconnor663 5y ago> I haven't been able to reconcile to myself how this isn't just an illusion of overall safety. Experienced Rust writers try to be very clear about this, but it's a subtle point that's hard to fit into an elevator pitch for the language. Safety in Rust is an encapsulation mechanism, and it's closely related to privacy. In fact, we can use privacy as a good metaphor. Suppose we have a private member variable x, in any language that supports such a thing. And let's say the design of our class is such that x should always be less than 10. Does the fact that x is private mean that we're guaranteed it will always follow that invariant? No of course not, because the public methods of our class could have bugs in them that screw up the value of x. However, we still get a useful guarantee here! We're guaranteed that x can only violate its invariants if our methods have a bug. We don't have to worry about what any specific caller is going to do, because privacy rules let us make guarantees about x solely based on our code. Safety in Rust is similar. If Vec is buggy and unsound, then its "safe API" isn't providing much value. But if I manage to cause UB using Vec, that is a bug report for the Rust standard library, and they will fix it. Once the bug is fixed, then my safe calling code cannot cause UB using Vec, no matter how hard it tries. These end up being very useful guarantees in practice. Many nontrivial programs can be written entirely in safe code, using only high-quality dependencies that get a lot of testing. Surprise soundness holes come up occasionally, but they're kind of like miscompliation bugs in that it's usually hard to trigger them.
- maxwell86 5y ago> To review, where do Rust and C++, these programming languages with their vastly different philosophies, Rust for the cautious, C++ for the fast and bold, stand? In the exact same place. The author spent 1 page before this statement, and the whole article after it, explaining that this is not true, so the article is a big contradiction. Rust and C++ are not "in the exact same place". With Rust, you get bound checking by default. If, after profiling, you find that it is a performance problem somewhere, it allows you to elide it safely. In the programs I work on, 99% of the execution time of my program is spent in 1% of the code, and Rust optimizes for this situation. Instead of debugging segmentation faults due to performance optimizations that buy you nothing in 99% of the code, you can spend your time optimizing the 1% that actually makes a difference. This is why Rust libraries are program are "so fast". Its not because of multi threading, or because rust programmers are geniuses, but rather because Rust buys these programmer time to actually optimize the code that matters, and in particular, do so without introducing new bugs.
- robalni 5y agoI don't know what people mean when they talk about "safe" or "unsafe" code. Doing something like `int a[5]; a[2] = 100;` in C is perfectly safe because there is no bug in that code. The only thing that might be unsafe about that is if you change the code because then you might create a bug. Changing code is always unsafe because you can always create bugs in any language, even Rust. I don't think "safe" or "unsafe" can be a property of code; it can only be a property of something you do, like changing code. I think that something being "unsafe" means that there is a risk with doing it. Programming is always a risk, even if you write Rust code without using the "unsafe" keyword. You can even have arbitrary code execution bugs in Rust programs without using the "unsafe" keyword; think about bugs like SQL-injections. All of this doesn't mean than I don't think the checks that the Rust compiler does help. They probably help many people to write less buggy code. I just think it makes no sense to call code "safe" or "unsafe".
- adgjlsfhk1 5y agoThis is only true if you never pass arrays to functions. Once you have a function that takes in an array and does indexing, it is possible to ask if it ever can access memory it wasn't supposed to without changing that function.
- robalni 5y agoTo add some more explanation; there are layers of safety. The arrays in Rust or any other language are a layer on top of the memory pages that you get from the operating system. Just like that, you actually have bounds checks in C because the operating system has bounds checks on the memory pages that you use; the safety is just on a lower level. Languages like Rust add a layer of safety on top of the operating system's layers. The problem is that even if you have safety on one layer, the next layer will always be unsafe, and as long as you have abstractions in your code, you will always have layers. Let's say you build some kind of abstraction on top of Rust arrays. The compiler will do bounds checks on the arrays but your abstraction will have no checks unless you implement them. Let's say that some state of your abstraction is invalid; the compiler will not help you to check that. Therefore you can't have a safe language, because even if one layer is perfectly safe, as soon as you add an abstraction layer, you have no safety checks on that layer. SQL injections are an example of that; even if SQL were a perfectly safe language, as soon as you add a layer on top of that (a function that builds SQL code by concatenating strings) you are back to no safety.
- pizlonator 5y agoI’m my view, Rust is a very uninspired kind of safe language. But a point on which I agree with Rust is that array accesses should be checked by default. I think this article ignores some arguments for array bounds checks and it ignores the importance of what the default is: - It doesn’t matter how fast or slow bounds checking is in theory. It only matters how fast it is in practice. In practice, the results are quite surprising. For example, years ago WebKit switched its Vector<> to checking bounds by default with no perf regression, though this did mean having to opt out a handful of the thousands of Vector<> instantiations. Maybe this isn’t true for everyone’s code, but the point is, you should try out bounds checking and see if it really costs you anything rather than worrying about hypothetical nanoseconds. - If you spend X hours optimizing a program, it will on average get Y% faster. If you don’t have bounds checks in your program and your program has any kind of security story, then you will spend Z hours per year fixing security critical OOBs. I believe that if you switch to checking bounds then you will instead get Z hours/year of your life back. If you then spend those hours optimizing, then for most code, it’ll take less then a year to gain back whatever perf you lost to bounds checks by doing other kinds of optimizations. Hence, bounds checking is a kind of meta performance optimization because it lets you shift some resources away from security to optimization. Since the time you gain for optimization is a recurring win and the bounds checks are a one time cost, the bounds checks become perf-profitable over time. - It really matters what the language does by default. C++ doesn’t check bounds by default. The most fundamental way of indexing arrays in C++ is via pointers and those don’t do any checks today. The most canonical way of accessing arrays in Rust is with a bounds check. So, I think Rust does encourage programmers to use bounds checking in a way that C++ doesn’t, and that was the right choice. As a C++ apologist my main beef is: if bounds checks are so great then please give them to me in the language that a crapton of code is already written in rather than giving me a goofy new language with a different syntax and other shit I don’t want (like ownership and an anemic concurrency story).
- rakingleaves 5y agoRelatedly, this recent paper shows that many manually-removed bounds checks in Rust libraries can be re-introduced with no bottom-line perf regression, depending on the application https://dl.acm.org/doi/10.1145/3485480 https://dl.acm.org/doi/10.1145/3485480