5 ms·
Your supposition about Rust is correct. I’ll add that—having paid that upfront cost—I am happily reaping the rewards even when I write code in other languages.
by stouset 2y ago
Your supposition about Rust is correct.
I’ll add that—having paid that upfront cost—I am happily reaping the rewards even when I write code in other languages. It turns out the way that Rust “wants” to be written is overall a pretty good way for you to organize the relationships between parts of a program. And even though the borrow checker isn’t there looking out for you in other languages, you can code as if it is!
- farresito 2y agoAs someone who is interested in getting more serious with Rust, could you explain the essence of how you should always approach organizing code in Rust as to minimize refactors as the code grows?
- oconnor663 2y agoIn my experience there are two versions of "fighting the borrow checker". The first is where the language has tools it needs you to use that you might not've seen before, like enums, Option::take, Arc/Mutex, channels, etc. The second is where you need to stop using references/lifetimes and start using indexes: https://jacko.io/object_soup.html https://jacko.io/object_soup.html
- meindnoch 2y ago>and start using indexes So basically raw pointers with extra hoops to jump through.
- nordsieck 2y ago> So basically raw pointers with extra hoops to jump through. That's one way to look at it. The other way is: raw pointers, but with mechanical sympathy. Array based data structures crush pointer based data structures in performance.
- jpc0 2y ago> Array based data structures crush pointer based data structures in performance Array[5] And *(&array + 5) generates the same code... Heap based non-contiguous data structures definitely are slower than stackbased contiguous data structures. How you index into them is unrelated to performance. Effectively pointers are just indexes into the big array which is system memory... I agree with parent, effectively pointers without any of the checks pointers would give you.
- frutiger 2y ago> pointers are just indexes into the big array which is system memory... I’m sure you are aware but for anyone else reading who might not be, pointers actually index into your very own private array. On most architectures, the MMU is responsible for mapping pages in your private array to pages in system memory or pages on disk (a page is a subarray of fixed size, usually 4 KiB). Usually you only get a crash if you access a page that is not currently allocated to your process. Otherwise you get the much more insidious behaviour of silent corruption.
- _dain_ 2y ago>How you index into them is unrelated to performance. Not true. If you store u32 indices, that can impose less memory/cache pressure than 64-bit pointers. Also indices are trivially serializable, which cannot be said for pointers.
- jpc0 2y agoI'll happily look at a benchmark which shows that the size of the index has any significant performance implications vs the work done with the data stored at said index, never mind the data actually stored there. I haven't looked closely at the decompiled code but I wouldn't be surprised if iterating through a contiguous data structure has no cache pressure but is rather just incrementing a register without a load at all other than the first one. And if you aren't iterating sequentially you are likely blowing the cache regardless purely based on jumping around in memory. This is an optimisation that may be premature. EDIT: > Also indices are trivially serializable, which cannot be said for pointers Pointers are literally 64bit ints... And converting them to an index is extremely quick if you want to store an offset instead when serialising. I'm not sure if we are missing each other here. If you want an index then use indices. There is no performance difference when iterating through a data structure, there may be some for other operations but that has nothing to do with the fact they are pointers. Back to the original parent that spurred this discussion... Replacing a reference (which is basically a pointer with some added suger) with an index into an array is effectively just using raw pointers to get around the borrow checker.
- quotemstr 2y agoYep. The array index pattern is unsafe code without the unsafe keyword. Amazing how much trouble Rust people go through to make code "safe" only to undermine this safety by emulating unsafe code with safe code.
- School-Cotton 2y agoThe difference is that the semantics of your program are still well-defined, even with bugs in index-based arenas.
- quotemstr 2y agoThe semantics of a POSIX program are well-defined under arbitrary memory corruption too --- just at a low level. Even with a busted heap, execution is deterministic and the every interaction with the kernel has defined behavior --- even if they behavior is SIGSEGV. Likewise, safe but buggy Rust might be well-defined at one level of abstraction but not another. Imagine an array index scheme for logged-in-user objects. Suppose we grab an index to an unprivileged user and stuff it in some data structure, letting it dangle. The user logs out. The index is still around. Now a privileged user logs in and reuses the same slot. We do an access check against the old index stored in the data structure. Boom! Security problems of EXACTLY the sort we have in C. It doesn't matter that the behavior is well-defined at the Rust level: the application still has an escalation of privilege vulnerability arising from a use-after-free even if no part of the program has the word u-n-s-a-f-e.
- IX-103 2y agoUndefined behavior in C/C++ has a different meaning than you're using. If a compiler encounters a piece of code that does something whose behavior is undefined in the spec, it can theoretically emit code that does anything and still be compliant with the standards. This could include things like setting the device on fire and launching missiles, but more typically is something seemingly innocuous like ignoring that part of the code entirely. An example I've seen in actual code: You checked for null before dereferencing a variable, but there is one code path that bypasses the null check. The compiler knows that dereferencing a null pointer is undefined so it concludes that the pointer can never be null and removes the null checks from all of the code paths as an "optimization". That's the C/C++ foot-gun of undefined behavior. It's very different from memory safety and correctness that you're conflating it with.
- throwawaymaths 2y agoand you now have unchecked use-after-decommisioning-the-index and double-decommission-the-index errors, which could be security regressions
- estebank 2y agoThat's true only if you use Vec<T> instead of a specialized arena, either append only, maybe growable, or generational, where access invalidation is tracked for you on access.
- oconnor663 2y agoYeah if you go with Vec, you have to accept that you can't delete anything until you're done with the whole collection. A lot of programs (including basically anything that isn't long running) can accept that. The rest need to use SlotMap or similar, which is an easy transition that you can make as needed.
- oconnor663 2y agoSort of. But you still get guaranteed-unaliased references when you need them. And generational indexes (SlotMap etc) let you ask "has this pointer been freed" instead of just hoping you never get it wrong.
- simgt 2y ago> stop using references/lifetimes and start using indexes Aren't arenas a nicer suggestion? https://docs.rs/bumpalo/latest/bumpalo/ https://docs.rs/bumpalo/latest/bumpalo/ https://docs.rs/typed-arena/latest/typed_arena/ https://docs.rs/typed-arena/latest/typed_arena/ Depending on the use case, another pattern that plays very nicely with Rust is the EC part of ECS: https://github.com/Ralith/hecs https://github.com/Ralith/hecs
- oconnor663 2y agoYes, Slab and SlotMap are the next stop on this train, and ECS is the last stop. But a simple Vec can get you surprisingly far. Most small programs never really need to delete anything.
- saghm 2y agoI'm fortunate enough not to have to often write code in other languages anymore, but my experience that writing code in ways that satisfies the compiler actually ends up being code I prefer anyhow. I was somewhat surprised at the first example because I haven't run into something like that, but it's also not really the style I would write that function personally (I'm not a big fan of repetitions like having `Some(x)` repeated both as a capture pattern and a return value), so on a whim I tried what would have been the way I'd write that function, and it doesn't trigger the same error: fn double_lookup_mut(map: &mut HashMap<String, String>, mut k: String) -> Option<&mut String> { map.get_mut(&k)?; k.push_str("-default"); map.get_mut(&k) } I wouldn't have guessed that this happened to be a way around a compiler error that people might run into with other ways of writing it; it just genuinely feels like a cleaner way for me to implement a function like that.
- khold_stare 2y agoIsn't that the opposite of the intended implementation? I don't write Rust, but I think your implementation will always return either `None` or the "fallback" value with the `"-default"` key. In the article, the crucial part is that if the first `map.get_mut()` succeeds, that is what is returned.
- brabel 2y agoA great example of how "if it compiles, it runs correctly" is bullshit.
- unshavedyak 2y agoYou're reaching pretty hard there. Your assertion is a massive strawman, the implication seeming to be that "every problem in your logic won't exist if it compiles" - no one thinks you can't write bad logic in any language. Rather it's about a robust type system that gives you tooling to cover many cases at compile time. Even if we ignore logic, Rust has plenty of runtime tooling and runtime issues can still happen as a result. A complaint i often have about Bevy (despite loving it!) is that it has a lot of runtime based plugins/etc which i'd prefer to be compile time. Axum for example has a really good UX while still being heavily compile time (in my experience at least). "If it compiles it works" is still true despite my complaints. Because i don't believe the statement even remotely implies you can't write bad logic or bad runtime code in Rust.
- ajb 2y agoIt sounds like the ideal, then, would be to detect the problematic patterns earlier so people wouldn't need to bang their heads against it.
- tonyarkles 2y agoI had a similar experience with Erlang/Elixir. The primary codebase I work with in $DAYJOB is C++ but structured very OTP-like with message passing and threads that can crash (via exceptions, we’re not catching defaults for example) and restart themselves. Because of the way we’ve set up the message passing and by ensuring that we don’t share other memory between threads we’ve virtually eliminated most classes of concurrency bugs.
- pdimitar 2y agoThat's the main use-case of Erlang/Elixir anyway: eliminate concurrency / parallelism bugs by copying data in the dangerous places, and make sure not to use locks but message passing instead. These two alone have eliminated most of the bugs I've wrote in other languages. So, same experience. And it taught me to be a better Golang and Rust programmer, too.
- rikthevik 2y agoThis is why I think cross-training is so important and I should do more of it. Even something relatively minor, like doing the Advent of Code in elixir (nim last year) has made my python significantly easier to reason about. If you quit mutating state so much, you eliminate whole classes of bugs.
- ngrilly 2y agoThat's also how I tend to program after having read Joe Armstrong's dissertation "Making reliable distributed systems in the presence of sodware errors" and having programmned in Go for a few years.
- ajross 2y ago> It turns out the way that Rust “wants” to be written is overall a pretty good way for you to organize the relationships between parts of a program That's what it promised not to do, though! Zero cost abstractions aren't zero cost when they force you into a particular design. Several of the cases in the linked article involve actual runtime and code size overhead vs. the obvious legacy/unchecked idioms.
- oconnor663 2y ago> vs. the obvious legacy/unchecked idioms You can go crazy with legazy/unchecked/unsafe stuff if you want to in Rust. It's less convenient and more difficult than C in some ways, but 1) it's also safer and more convenient in other ways, and 2) "this will be safe and convenient" isn't exactly the reason we dive into legacy/unchecked/unsafe stuff. And of course the greatest strength of the whole Rust language is that folks who want to do crazy unsafe stuff can package it up in a safe interface for the rest of us to use.
- ajross 2y ago> crazy unsafe stuff The first example in the linked article is checking if a value is stored in a container and doing something different if it's not than if it is. Hardly "crazy unsafe stuff".
- oconnor663 2y agoI think there's an important distinction here. A systems programming language needs to have all the max speed / minimum overhead / UB-prone stuff available, but that stuff doesn't need to be the default / most convenient way of doing things. Rust heavily (both syntactically and culturally) encourages safe patterns that sometimes involve runtime overhead, like checked indexing, but this isn't the same as "forcing" you into these patterns.
- ajross 2y agoAnd I can only repeat verbatim: The first example in the linked article is checking if a value is stored in a container and doing something different if it's not than if it is. Hardly "max speed / minimum overhead / UB-prone stuff"
- kazinator 2y agoWhy would you cling to some cockamamie memory management model, where it is not required or enforced? That's like Stockholm Syndrome.