5 ms·
As 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 mi
by farresito 2y ago
As 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.
- deleted 2y ago[deleted]
- 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.