Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

This makes it sound like Go and Rust have some secret sauce that enables C's performance without UB. But it is not so.

For example, consider an expression like (x*2)/2. clang and gcc will both optimize this to just x, but Go and Rust will actually perform the multiplication and division, as required by their overflow semantics. So the performance vs safety tradeoff is real.



I can't speak for Go, but your intuition regarding Rust code is incorrect. See https://play.rust-lang.org/?gist=09b464627f856e0ebdcd&versio... , click the "Release" button, then click the "LLVM IR" button to view the generated code for yourself. TL;DR: the Rust code `let x = 7; let y = (x*2)/2; return y;` gets compiled down to `ret i32 7` in LLVM.

In fact, there is "secret sauce" here. The secret sauce is that Rust treats integer overflow specially: in debug mode (the default compilation mode), integer overflow is checked and will result in a panic. In release mode, integer overflow is unchecked. It's not "undefined behavior" in the C sense, because of the fact that it always returns a value--there are no nasal demons possible here (Rust disallows UB in non-`unsafe` code entirely, because memory unsafety is a subset of nasal demons). The exact value that it returns is unspecified, and the language provides no guarantee of backward compatibility if you rely on it (it also provides explicit wrapping arithmetic types if you explicitly desire wrapping behavior). And though it's a potential correctness hazard if you don't do any testing in debug mode, it's not a memory safety hazard, even in the unchecked release mode, because Rust's other safety mechanisms prevent you from using an integer like this to cause memory errors.


A constant expression is not a good test: any compiler worth its salt (which includes LLVM) will be able to optimise a case like that. Change the function to:

  pub fn test(x: i32) -> i32 {
      let y = (x*2)/2;
      y
  }
and you'll see the difference (e.g. compare against a similar function on https://gcc.godbolt.org/ ).

> The secret sauce is that Rust treats integer overflow specially

Debug vs. release mode is irrelevant: the panicking case is more expensive than any arithmetic, and, the RFC[0] was changed before it landed:

> The operations +, -, [multiplication], can underflow and overflow. When checking is enabled this will panic. When checking is disabled this will two's complement wrap.

The compiler still assumes that signed overflow may happen in release mode, and that the result needs to be computed as per two's complement, i.e. not unspecified.

[0]: https://github.com/rust-lang/rfcs/blob/master/text/0560-inte...


Bah, darn you for changing the RFC out from under me. :P

I don't understand the point of specifying wrapped behavior in unchecked mode rather than leaving the value unspecified. Surely we don't care about properly accommodating use cases that will panic in debug mode.


It makes it easier to have assurances about the behaviour of the program, even in error cases, https://github.com/rust-lang/rfcs/pull/560#issuecomment-6999...


I finally understood your point and where I was unclear, mostly after reading the other comment about the for loop.

You're right that C requires UB to optimize things like (x2)/2. My argument is that Go and Rust's secret sauce is you wouldn't have to write things spiritually similar to that in the first place. (x2)/2 is a bad example, since nobody would write that on purpose, but loops are a great one. C's only interface to elements of an array is a pointer. So C has to say that accessing out-of-bounds pointers are UB, as is even having a pointer that's neither in-bounds or right at the end, because it simply has no way to distinguish "pointer" from "pointer that is in-bounds for this array". The type of an array iterator in Rust (and I think also Go) carries knowledge of what array it's iterating over, and how far it can iterate; since it's part of the type system, the compiler has access to that knowledge. So you can just say `for element in array`, and the compiler knows that you're only accessing in-bounds pointers, and generate the same code C would, without needing to define a concept of UB.

Of course if you do generate pointers in unsafe code, you are subject to UB, same as in C. Rust and Go simply give you a language where most of the time, you don't need to reach for constructs that require UB to be performant.

(There is, however, an actual bit of secret sauce, at least in Rust but I suspect Go has an analogue: Rust's ownership system allows it to do far stronger alias analysis than even C's -fstrict-aliasing, without the risk of false positives -- you cannot construct overlapping mutable pointers in safe code.)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: