Live data from Hacker News

Rust 1.45

blog.rust-lang.org

131–140 of 227 posts

Re: Rust 1.45

#131
post #116

Earlier quoted context omitted.

Do you have specification of a memory model?

I believe it's safe to assume that Rust's memory model is (a subset of) C++11's memory model, since that's what LLVM implements. At this point I don't see how any specification could deviate significantly from that without breaking tons of code.

The Rust memory model is deviating from it a little in order to enable some norestrict-based optimizations that aren't really done for C, even though (as you know) LLVM can't really take advantage of them yet.

Re: Rust 1.45

#132
post #102

Earlier quoted context omitted.

It is't about performance because array indexing also panics.

Compilers are better at eliding array index checks than about eliding numeric overflow checks; an ordinary function that operates on arrays will often be able to reasonably determine both the length of the array and the scope of the indexing variable (Rust's iterators are very good at this), whereas an ordinary math function often has almost no information that usefully limits which values it might be getting called…

They aren't unforgiving, because if you need performance you can use another function that does not do the check. Same as for arrays.

It seems strange to have a cast as a safe operation and yet return results that are almost surely a bug. This means I will avoid casts altogether in my code, and I do hope they get rid of them at some point as some have suggested.

Re: Rust 1.45

#133
Can we please deprecate the "as" operator?

Something so lossy and ill-conceived should not be a two-letter operator.

Re: Rust 1.45

#134
post #34

Earlier quoted context omitted.

Is this undergrad? Genuinely curious, how will you get someone to understand what ownership helps avoid without them having experienced the pain on the other side? I guess with younger and younger kids learning programming these, may be there can handle more? I am not sure if my son would understand all of the intricacies in his first semester.

> Genuinely curious, how will you get someone to understand what ownership helps avoid without them having experienced the pain on the other side? This is an eternal debate about Rust. I don't think it's required though. Can you appreciate functions without understanding assembly and calling conventions? I believe the answer is yes :)

I actually find that people who aren't that familiar with programming are pretty quick to accept the idea that aliasing xor mutable is an okay rule... not because it's been exhaustively explained, but just because people have no real expectations at all about how things should work when they're starting out. A lot of language rules seem arbitrary at that point. Afterwards, when using other languages, they may even find it surprising that you can mutate stuff without this rule... it's all about what you're used to.

Re: Rust 1.45

#135
post #30

Earlier quoted context omitted.

Great news, but it's not even 1.0 yet. Thus, it isn't stable enough for production use. At the moment, I'd rather use something like actix-web instead.

That's not how versions work

>= 1.0 in semantic versioning universally means that it should be "stable enough"

Re: Rust 1.45

#136
post #125

Earlier quoted context omitted.

Be aware that Rust gives you the tools to be fast, it is not necessarily fast by default, although a lot of constructs it guides you towards usually help with that. You still need to profile your code to see what you need to optimize, whereas other languages with fewer knobs will perform optimizations that you otherwise need to manually annotate in your code in Rust. I prefer this approach, but it can be surprising t…

Do you have examples of this? I'd be curious to know if so. (I've played w/ Rust a little bit -- I implemented a Boggle board scorer + high-scoring board generator; Rust outperformed my C++ code! I was impressed.)

One example of the choice you have is how you can deal with generic data types:

  fn foo(_: T){}
  fn foo(_: impl Trait) {}
  fn foo(_: &Trait) {}
These three different fn definitions have two different behaviors and affect both the speed of the code and the speed of compilation and it depends entirely on how they are called.

The first one is what the language calls generics: they are always monomorphized, which means that if you have three calls to `foo` with different types (that implement Trait) the compiler will expand three different functions with different types (code expansion).

The second one is a separate syntax level feature (impl Trait) which was mainly added to introduce a new feature which is static opaque types, where the function determines what the underlying the return type will be, but the caller can only interact with it using the trait's API.

[Aside] This is useful for cases like the following:

  fn it() -> impl Iterator {
      vec![1, 2, 3].into_iter()
  }
where you would otherwise have to specify the specific type:

  fn it() -> std::vec::IntoIter {
      vec![1, 2, 3].into_iter()
  }
This example doesn't seem like much, but if you want to add a `map()` call to this you start to see the benefit:

  fn it() -> impl Iterator {
      vec![1, 2, 3].into_iter().map(|x| x * x)
  }

  fn it() -> std::iter::Map, fn(i32) -> i32> {
      vec![1, 2, 3].into_iter().map(|x| x * x)
  }
The more types you nest the more the benefits come into play. [end of aside]

Now, with that out of the way, the type of an impl Trait in argument is decided by the caller (not the function), so they are implemented internally exactly the same as type generics. The only difference is arguable nicer syntax in the definition and not being able to specify a type using the turbofish. For all intents and purposes, those two are the same feature.

The third function is different, it uses a virtual table, with everything that implies: there's type erasure, there's only a single function in the expanded code (which makes compilation faster because the compiler doesn't need to do work), calling this function can be slower because the final executable has to perform some pointer chasing to call methods, instead of directly knowing where to call them.

All of this to say: if you use `fn foo(_: T)` or `fn foo(_: &Trait)` affects compilation and execute time, so you have to be aware of their distinction. This means that if you're not aware you might have slower code than you would with a compiler (like Swift, for example) which relies on heuristics to decide to do static or dynamic dispatch, but it also means that your code's performance characteristics won't change all of a sudden because you modified a tangentially related part of the code and suddenly crossed some threshold.

Another example can be `.clone()`: is it slow? The answer is always "it depends". You might be cloning an `Arc`, which is cheap, you could be cloning a 10MB string, which is slow. But because we train ourselves to see clone as slow we might be worried or annoyed by `Arc`. We could make it `Copy`, but if we did that then you have less control over where the `Arc` gets copied which would make it harder to keep track of where the RC gets incremented. The language also doesn't automatically implement `Copy` for small structs, even though it could, which would make it easier to learn that part of the language (you don't learn to add derives early on), at the cost of baffling behavior (you might add a field and suddenly your struct isn't considered "small" anymore).

Yet another example, you also have access to `Cow`, which lets you deal with both static and heap allocated strings in the same way in your code, but it pollutes your code, where the naïve thing to do would be to use `String` everywhere.

My personal wish is for Rust to remain explicit as much as possible, but use lints to emit suggestions for the cases where a more "magic" language would change the emitted code. That way the code documents its behavior with fewer surprises.

Re: Rust 1.45

#137

This rather niche fixing of unsafe behaviour is excellent: https://blog.rust-lang.org/2020/07/16/Rust-1.45.0.html#fixin... I spent a few years as a scientific programmer and this is exactly the sort of thing that just bites you on the behind in C/C++/Fortran: the undefined behaviour can actually manifest as noise in your output, or just really hard to track down, intermittent problems. A big win to get rid of it.

It would be nice if there were both a saturating-as and a overflow-is-a-bug-as, the later of which is also saturating but in debug builds get instrumentation to panic if it ever actually saturates.

The overflow-is-a-bug-saturating as would be the default, and there would be a separate sat_as for "I know this saturates, it isn't a bug.". ::sigh:: Rust went through this same debate for integers, initially rejecting the argument I'm giving here but switching back to it after silently-defined-integer-overflow concealed severe memory unsafty bugs in the standard library.

Well-defining something like saturation actually reduces the power of static and dynamic program analysis because it can no longer tell if the overflow was a programmer-intended use of saturation or a bug.

Having it undefined was better from a tools perspective, even if worse at runtime, because if a tool could prove that overflow would happen (statically) or that it does happen (dynamically) that would always be a bug, and always be worth bringing to the user's attention.

So now you still get "noise" in your output, but it's the harder to detect noise of consistent saturation, and you've lost the ability to have instrumentation that tells you that you have a flaw in your code.

So I think this is again an example of rust making a decision that hurts program correctness.

Re: Rust 1.45

#138

Earlier quoted context omitted.

If you want to omit a bounds check, the compiler needs to know that the length of the array covers the upper bound of the loop, right?

If the array length is known to be strictly less than 255 then there is definitely an out-of-bounds access inside the loop, but since this is a panic rather than undefined behavior it could matter how many loop iterations are executed before the out of bounds access occurs, so the check can't be omitted. If the array size is definitely greater than or equal to 255 then all the array accesses in the loop will be in bo…

Oh, right.

Re: Rust 1.45

#139
post #133

Can we please deprecate the "as" operator? Something so lossy and ill-conceived should not be a two-letter operator.

It is possible, someone needs to do the RFC work.

I would say that my personal take of the temperature is "vaguely pro but not a slam dunk", at least from the opinions I've seen. Only one way to find out.

Re: Rust 1.45

#140

Earlier quoted context omitted.

It should be `assert!(len(arr) >= 255)` (greater instead of less than), right?

Assuming a unsigned byte, that range of values is between 0 and 255 inclusive, so `len(arr) <= 255` is correct.

But the loop goes up to 255. So if len(arr) == 10, then assert!(len(arr) <= 255) woulds succeed, but you'd get an out-of-bounds access if you tried to access arr at 11.
Post reply on HN