6 ms·
Rust’s worst feature
- 0x1ceb00da 2y ago> Even an obvious optimisation of moving the buffer declaration outside of the loop isn’t available to the compiler. Why? Can't the programmer just do this himself?
- Arch-TK 2y agoThe compiler cannot assume that the read call won't read from the mutable reference (well, it might be able to given a sufficiently sophisticated optimizer and/or aggressive inlining). The programmer, on the other hand, can do this, but the point is to make this implicitly possible by making it more explicit that read does not read from the buffer (and therefore allowing it to accept uninitialized memory).
- dietr1ch 2y agoAnd I don't think it can ensure that all the bits were written. I've been bit by people trying to reuse buffers/objects like this that were not fully rewritten in one of the possible re-uses. It's a bit puzzling how a change that just adds a new continue leads to memory corruption.
- hansvm 2y agoZig has a couple features to help with that. I assume Rust should too (probably not directly applicable to BorrowedBuf, but for the case of a reusable pool of objects)? It comes down to a piece of syntactic sugar, plus "result location semantics" guaranteeing that you won't have a copy. E.g.: my_ptr.* = .{ .x = 42, .y = 53 }; No matter how you choose to construct the intermediate fields (x and y in this example) with continues or other control flow, the very last step should be something that sets every field at once. If you miss one, the compiler will yell at you. If it compiles, the assembly is as if you filled in each field by hand.
- dietr1ch 2y agoIt's cool to have syntax for that, but I feel that the optimisation there works at a too high level. I'd like the backend to notice that a variable is being dropped and "re-created" on every iteration, and then figuring out how to initialise over a "dropped" zombie value. It'd be nice to have something like this, because I'm pretty sure people don't do this kind of optimisation all the time (it's annoying to leak the variable that should survive loops outside of the loop becase it's not mentioned elsewhere).
- hansvm 2y agoThat's another possible tradeoff. Suppose you want that feature and aren't giving any more information to the compiler than you normally do, two extremes exist: 1. Add any such pools you can reason about at compile-time (Rust does this, or at least it did the last time I profiled anything rusty at my last job). 2. Dynamically infer how such pools should behave at runtime. (1) has the same sorts of problems you see with auto-vectorization. It gives a modest boost for 'free' (Rust's implementation when I benchmarked it was sub-par, and a naive user-space pool would have been better), but it won't kick in that often. Perf wins for 'free' often aren't a bad thing. (2) isn't terrible if you're okay with a GC language. Many GC's effectively do something similar internally. The code complexity is similar. However, both ideas also have the potential to fall prey to the issue we were trying to circumvent in the first place. The "backend notic[ing]" only covers the performance aspect of the equation. The real value is in having a dedicated syntax for compiler-erroring partial initializations, combined with the knowledge that such code is free (compared to less-safe state-of-the-art options) at runtime. I think you're observing that APIs like in TFA are easy to mis-use, and my proposal says "make them easy to use," where you're proposing "make the compiler smart enough to not need them"?
- Waterluvian 2y agoArguably the buffer belongs in the loop scope because it is only relevant there. It’s probably also safer from wrong use. This feels like exactly what you want the compiler to think about: a case where optimization comes at the cost of organization.
- immibis 2y agoOrganization is basically completely irrelevant for a small piece of code like this, which fits in your head all at once.
- lmm 2y ago> Why? Because as far as the compiler is concerned it appears to change the behaviour, unless the compiler gets very fancy. > Can't the programmer just do this himself? Yes, but it's not really desirable for them to have to (and would arguably make the code less maintainable if they did). Doing the right thing should be easy.
- auggierose 2y agoNot if they are using Rust ... which is why I am not.
- vlovich123 2y agoWhat language would automatically be able to hoist the array outside the loop in that kind of code?
- jhasse 2y agoC, because reading uninitialized memory is undefined behavior so the compile can assume it never happens.
- jeroenhd 2y agoBut wouldn't that change behavior? An empty, zero initialised array will contain data and a bunch of zeroes after a read, but if the next read only partially fills the buffer you end up with a buffer containing data from two reads. In this specific example there's no issue because the result of read() is being used to only write as much data as was read, but to me this seems like a pretty complicated and unlikely assumption to write optimizations for.
- vlovich123 2y agoCare to present some proof? Here’s an counter proof that the compiler isn’t able to reason about the memory in that way https://godbolt.org/z/x7j8xoMxY https://godbolt.org/z/x7j8xoMxY There are cases where C can do loop hoisting, but the cases are a subset of what Rust does and this isn’t one of those.
- uecker 2y agoYou example doesn't show this. Also the allocation is hoisted out of the loop. The initialization is not and this would be invalid in general. It could eliminated in this case, but this would be dead store elimination.
- vlovich123 2y agoBecause the compiler doesn’t know what read/write are doing to the buffer. And since it’s declared as [0; 4096], the compiler wouldn’t be able to do anything other than 0’ing the entire 4kib region on every read instead of what’s dirtied if it attempted to automatically hoist. BorrowedBuf is an attempt to let you declare [MaybeUninit::uninit(); 4096] which the compiler could hoist although there it doesn’t matter either since the allocation of the uninit array is just an adjustment of the stack pointer.
- mmastrac 2y agoThis API has basically been adopted from Tokio. Like most of Rust buffer types, it's "not bad" to use as a caller and "awkward" to use as a consumer. The pain of paying for buffer init is real, however. The last two projects have both seen perf hits from it.
- tmtvl 2y agoI'm not a Rust person (give me Lisp any day), but this stuck out to me: > A motivated programmer can try adding necessary support to actively maintained packages [...] but what if one is stuck at an older version of the crate or deals with apparently abandoned crates One could maybe do some programming? I mean, hell, if most of the work has already been done for you, then what's holding you back? Besides, why would you want to use outdated, bug-ridden libraries filled with vulnerabilities instead of something well-maintained?
- layer8 2y agoThe notion that a library must be “bug-ridden” and “filled with vulnerabilities” just because it hasn’t changed in a while is a strange one. If it were true, it would also be true for libraries that do change. It’s not like libraries accumulate new bugs and vulnerabilities by not changing. Libraries that constantly gain new features, on the other hand, are prone to also gain new bugs and vulnerabilities.
- b112 2y agoAgree, I think we need to speak of abandoned, versus maintained. No activity doesn't mean abandoned, it can just mean super stable and few bugs to fix. Abandoned is a big worry though. I wonder if we need a canary file, but in the repo. An "I'm still here" file.
- layer8 2y agoIf a library is stable with no major bugs, then I don’t see much of a difference between abandoned and maintained. The one thing I would look at instead is open issues.
- b112 2y agoAbandoned means no one around to deal with security issues, or impacting bugs. Maintained means they are. There may be no new features though. (these are my definitions, but I think there should be similar concepts in all dev's heads)
- lmm 2y agoWhy is it that the frozen semantics are actually needed? Is there no way to represent what people actually want here - memory that is entirely uninitialised, for which tautology might be false, until written? I.e. something that's a bit like MaybeUninit but more so?
- api 2y agoThere's probably no way for the compiler to prove safety. Rust is designed to allow 100% safe bare metal development, like a perfectly safe C that still allows you to get close to the hardware, and that's tough.
- ajross 2y agoI'm failing to understand the correlation to "safety" here. Reading a byte for which you don't know the value isn't "unsafe". It's literally (!) the desired behavior of foreign data being read from an external source, which is in fact the use case in the article. There's no safety problem as long as the arbitrary value is deterministic, which it is, being process RAM. The "uninitialized data read" bugs reported from instrumentation tools in C code are because the code is assuming the value has some semantics. The read itself has no value and is presumably an artifact of the bug, but it is safe.
- gpm 2y ago> There's no safety problem as long as the arbitrary value is deterministic, which it is, being process RAM. The article discusses how it is in fact, on Linux with memory returned from at least one very common allocator, not deterministic. Ctrl-f tautology.
- ajross 2y agoThat's just a terminology collision. All RAM access is deterministic in the sense that the value will not change until written. It's not "predictable" in the sense that the value could be anything. C code that reads uninitialized data is presumed to be buggy, because it wouldn't be doing that unless it thought the memory was initialized. But the read itself is not unsafe. Rust is just confused, and is preventing all reads from uninitialized data a-priori instead of relying on its perfectly working type system to tell it whether the uninitialized data is safe to use. And that has performance impact, as described in the linked article, which has then resulted in some terrible API choices to evade.
- leni536 2y agoDo some Rust types have invalid object representation or trap representation? On SysV x86_64 bool only has two valid representations in memory, the rest are trap representations. So for an array of bools (if Rust matches SysV) freeze wouldn't be sound, even without the madvise problem.
- gpm 2y agoYes. That's what "// SAFETY: u8 has no invalid bit patterns." is discussing. That while types in general can u8 specifically does not (none of the u*/i* integer types do) so freezing a buffer of u8s is sound.
- kmeisthax 2y agoYes; and furthermore LLVM also has undef, which is sort of a trap representation, but it only exists in the optimizer. (There's also poison for overflow, which is a strictly less defined value than undef.) I suspect even reading an array of uninitialized u8s would cause havoc just from LLVM miscompiles alone.
- queuebert 2y agoThe more pertinent question to me is can we implement some new static analysis that understands buffer re-use and can hoist buffer initialization outside the loop? Rather than make the programmer write obfuscated code for efficiency, it is usually better to have the compiler do the heavy lifting. P.S. Also, folks, don't re-use buffers without zeroing unless you absolutely need the performance and know what you're doing.
- vlovich123 2y agoI like that direction better but it requires the ability to declare data-flow based contracts whereas Rust’s tools are only lifetime and type contracts. Is there a language that has data-flow based contracts?
- ijustlovemath 2y agowhat do you mean by this?
- vlovich123 2y agoThere would need to be contractual declarations on the read method that the compiler is able to enforce that tells it that the input &mut slice has N elements clobbered based on the returned length. That’s basically what BorrowedBuf is accomplishing via the type system and runtime enforcement of the contract. Using a non-existent syntax: fn read<T, N: size_t>(&mut self, buf: &mut [MaybeUninit<T>] becomes &[T; N] after call) -> N { … enforces the body initializes N elements out of buf } and then rules that &mut [T] can also be supplied to such functions that today could only accept a &mut [MaybeUninit<T>] transparently. A more likely interface you could write today would look like: fn read_uninit<T>(&mut self, buf: &mut [MaybeUninit<T>]) -> (&[T], &[MaybeUninit<T>]) { … enforces the body initializes N elements out of buf } You still have to cast &[T] into &[MaybeUninit<T>] somehow.
- Someone 2y agoI think an ergonomic way to do that would to have read return not an integer, but a slice of that integer’s length. Problem would be: how do you express “you can only access the buffer you sent me through the read-only slice I returned, but you have to free that same buffer when you’re done calling me? I think that can be done using a function creating a read buffer for a given input stream that - during calls to read is ‘owned for writing’ by that stream (so, it has to borrow a capability that the creator of the buffer doesn’t have. I don’t think Rust currently supports that) - where stream.read returns a read only slice whose lifetime is bound to that of the buffer So, the creator of the buffer can only pass it to read to get a slice back that contains precisely the data read. The stream can write to the entire buffer.
- lincpa 2y ago[dead]
- DannyBee 2y agoThis seems not even close to the worst feature of rust - this seems like it needs some more design work and baking. Like lots of things. The amount of hyperbole in this article makes it a bit hard to take the author all that seriously. Is there evidence more baking won't happen? While i have my loves and hates about rust, it definitely always felt like they had a pretty thorough/careful process for additions like this. If you go constructively into the threads and offer some concerns, you will usually get some reasonable response. (All processes of course, fail, so this is not always true, but it's mostly true) While i think it's fine to write rants on blogs, and don't feel like everyone has a responsibility to file bugs or whatever before they write a rant about it, if you actually want to see this "worst feature" fixed, this probably won't help very much. (IE You don't have to be constructive, or even helpful, but if you want to be constructive or helpful, this ain't how you do it)
- Xunjin 2y agoI'm not as much low-level development as the author seems to be, but the hyperbole made me think “you have a point, which is/can be valid, but aren't you stretching the reasons to fit in your sentiment/PoV?” Being honest, plenty of times we throw “Ergonomics” as an argument, however, are ergonomics more a feeling of how good are the API usage instead of actually prove with examples and design choices?
- dathinab 2y ago> Is there evidence more baking won't happen? No, actually there is a lot of evidence that it will still be worked on. Normally I would just say look at the issue linked in the nightly docs but due to an overlap of tacking PR and moving it from std to core PR it's not supper useful. Tracking Issue: https://github.com/rust-lang/rust/issues/78485 https://github.com/rust-lang/rust/issues/78485 If the author has constructive critique they should probably mention it there (after skimming through the discussion to make sure this wasn't already considered and not done due to subtleties they overlooked in the blog post (like e.g. that it's a standard/core feature which has to work across all targets and as such can't rely on anything being initialized to 0 by the OS, or that depending on the global allocator used you definitely can't rely on things being zeroed even if the OS only hands out zeroed memory, etc. etc.))
- Zagitta 2y agoMaybe Rust needs another type of reference that's exclusive write only? Right now there's RO (&T) and exclusive RW (&mut T) but WO is missing. Having a WO reference would allow these read_buf APIs to express they only write and never read so the uninitialized memory is safe to pass directly.
- gpm 2y agoIn some sense that's exactly what a `&mut MaybeUninit<T>` is?
- mmastrac 2y agoProbably more once https://doc.rust-lang.org/beta/std/mem/union.MaybeUninit.html#method.copy_from_slice https://doc.rust-lang.org/beta/std/mem/union.MaybeUninit.htm... is no longer nightly-only.
- 1oooqooq 2y agoeveryone just tell you to use mpsr in this case
- breaker-kind 2y agowhat does the term "nightly" mean in this context?
- seanhunter 2y agoRust has “nightly” and “stable” toolchain variants called “release channels”. The “nightly” version is literally just the compiler and all the other tools built in the CI/CD pipeline on a nightly basis with the latest branch including various features that need further development before they go to stable. The process and reasoning are described here https://doc.rust-lang.org/book/appendix-07-nightly-rust.html https://doc.rust-lang.org/book/appendix-07-nightly-rust.html
- camdenreslink 2y agoIt is the version of rust that is under active development, and also contains experimental features that aren’t in the “stable” rust compiler.
- deleted 2y ago[deleted]
- jasonthorsness 2y agoI am not yet a Rust programmer but - is it not typical to have a small collection of unsafe functions, carefully reviewed, that in this case seem like they might be easier to maintain than some of these convoluted type-based workarounds?
- steveklabnik 2y agoIt's a tradeoff! You have to explore both options to know which side of the tradeoff to take.
- Matthias247 2y agoIf this is mainly useful for working with plain/uninterpreted byte arrays, then I wonder why we can't just do `[u8; N]::with_noinit()` method instead of doing the multi-line plus unsafe things listed in the article. Is the main point that things like `slice_freeze_mut` could also be used for slices of e.g. `struct Coordinate { x: u32, y: u32, z: u32 }`? It would obviously not work for f64 things, since there also not all bit-patterns are valid.
- orlp 2y agoAll f64 bit patterns are valid.
- School-Cotton 2y ago> While replacing the array of zeros by an array of uninitialised values may work in specific circumstances, the code is unsound. Change to the compiler, its options, modification of unrelated parts of the code or using the function for a different Read trait implementation may break the program in unpredictable ways. Why? It seems the only thing on that list that will cause UB is using the function with a different reader (one that inspects the uninitialized bytes). Why would any of the other listed possible changes break it?
- GrantMoyer 2y agoIn Rust, creating a reference to uninitialized data is undefined behavior, even if you don't access the data through it[1]. The optimizer assumes all references are intialized. [1]: https://doc.rust-lang.org/std/ptr/#pointer-to-reference-conversion https://doc.rust-lang.org/std/ptr/#pointer-to-reference-conv...
- GrantMoyer 2y agoFollow up to my earlier comment; The docs say creating a reference to uninitialized memory is undefined behavior, but I couldn't think of any cases where it would go wrong if the data is never actually accessed. It turns out that's because there currently aren't any. See comment [1] and the threads linked there. Rust calls references to uninitialized memory undefined behavior because it wants to reserve the right to implement some kinds of optimizations using that fact in the future, however currently no optimizations do, and in fact the standard library depends on the compiler being well behaved here. It's possible at some point in the future some optimization will be added which would cause problems for references to uninitialized memory, but it seems more likely that the requirements on references will eventually be relaxed instead. [1]: https://github.com/rust-lang/rust/issues/119241#issuecomment-1868235132 https://github.com/rust-lang/rust/issues/119241#issuecomment...
- steeeeeve 2y agoThe selling point of Rust was that it protects programmers from doing dangerous things.
- steveklabnik 2y agoThis is a good first approximation, but it misses something. It's actually that it protects programmers from accidentally doing dangerous things. There's a lot of support in the language for doing dangerous things, you just have to explicitly say "hey I know I'm doing something dangerous, and I promise I'm right here."
- pnathan 2y agoI think the answer is that in a case when you need that speed, you hoist the stack allocation & zeroing and unsafe that buffer in the loop if need be. Test well. I am a huge Rust fan but also it is actually possible to write correct unsafe code. If I am interacting with from IO space, I would much rather write the interaction code myself for the machine at hand than farm it out to an array of third party crates. ::shrug:: getting the machinery to let it properly be hoisted smoothly and safely would be nice, but it isn't required. personally I think rust macros are very painful and the "worst feature", but that's speaking as someone who did a fair bit of Common Lisp.