link to LLVM doc; mention bitfields
[web.git] / personal / _posts / 2019-07-14-uninit.md
index d3fb0fe41cfa9f69261497a47a2429258ffa12ca..1847e5eb5e6905e86d3dbbcc125bbe3e6d158fd0 100644 (file)
@@ -54,7 +54,7 @@ However, if you [run the example](https://play.rust-lang.org/?version=stable&mod
 ## What *is* uninitialized memory?
 
 How is this possible?
-The answer is that every byte in memory cannot just have a value in `0..256` (this is Rust syntax for a left-inclusive right-exclusive range), it can also be "uninitialized".
+The answer is that, in the "abstract machine" that is used to specify the behavior of our program, every byte in memory cannot just have a value in `0..256` (this is Rust/Ruby syntax for a left-inclusive right-exclusive range), it can also be "uninitialized".
 Memory *remembers* if you initialized it.
 The `x` that is passed to `always_return_true` is *not* the 8-bit representation of some number, it is an uninitialized byte.
 Performing operations such as comparison on uninitialized bytes is undefined behavior.
@@ -65,11 +65,13 @@ Compilers don't just want to annoy programmers.
 Ruling out operations such as comparison on uninitialized data is useful, because it means the compiler does not have to "remember" which exact bit pattern an uninitialized variable has!
 A well-behaved (UB-free) program cannot observe that bit pattern anyway.
 So each time an uninitialized variable gets used, we can just use *any* machine register---and for different uses, those can be different registers!
-So, one time we "look" at `x` it can be at least 150, and then when we look at it again it is less than 120, even though `x` did not change.
+[This LLVM document](http://nondot.org/sabre/LLVMNotes/UndefinedValue.txt) gives some more motivation for "unstable" uninitialized memory.
+So, one time we "look" at `x` it can be at least 150, and then when we look at it again it is at most 120, even though `x` did not change.
 `x` was just uninitialized all the time.
 That explains why our compiled example program behaves the way it does.
 
-When thinking about Rust (or C, or C++), you have to imagine that every byte in memory is either initialized to some value in `0..256`, or *uninitialized*.
+When thinking about Rust (or C, or C++), you have to think in terms of an "abstract machine", not the real hardware you are using.
+Imagine that every byte in memory is either initialized to some value in `0..256`, or *uninitialized*.
 You can think of memory as storing an `Option<u8>` at every location.[^pointers]
 When new memory gets allocated for a local variable (on the stack) or on the heap, there is actually nothing random happening, everything is completely deterministic: every single byte of this memory is marked as *uninitialized*.
 Every location stores a `None`.
@@ -77,7 +79,7 @@ Every location stores a `None`.
 
 When writing safe Rust, you do not have to worry about this, but this is the model you should have in your head when dealing with uninitialized memory in unsafe code.
 Alexis wrote a [great post](https://gankro.github.io/blah/initialize-me-maybe/) on which APIs to use for that in Rust; there is no need for me to repeat all that here.
-(In that post, Alexis says that every *bit* can be either 0, 1 or uninitialized, as opposed to every *byte* being initialized or not. Given that memory accesses happen at byte granularity, these two models are actually equivalent.)
+(In that post, Alexis says that every *bit* can be either 0, 1 or uninitialized, as opposed to every *byte* being initialized or not. Given that memory accesses happen at byte granularity, these two models are actually equivalent, at least in Rust which does not have C-style bitfields.)
 
 [^pointers]: In fact, [bytes are even more complicated than that]({% post_url 2018-07-24-pointers-and-bytes %}), but that is another topic.
 
@@ -91,15 +93,28 @@ Over time, we will come to some kind of compromise here.
 The important part (for both Rust and C/C++) however is that we have this discussion with a clear mental model in our minds for *what uninitialized memory is*.
 I see Rust on a good path here; I hope the C/C++ committees will eventually follow suit.
 
+Ruling out any operation on uninitialized values also makes it impossible to implement [this cute data structure](https://research.swtch.com/sparse).
+The `is-member` function there relies on the assumption that "observing" an uninitialized value (`sparse[i]`) twice gives the same result, which as we have seen above is not the case.
+This could be fixed by providing a "freeze" operation that, given any data, replaces the uninitialized bytes by *some* non-deterministically chosen *initialized* bytes.
+It is called "freeze" because its effect is that the value "stops changing each time you observe it".
+`is-member` would freeze `sparse[i]` once and then know for sure that "looking at it" twice will give consistent results.
+Unfortunately, since C/C++ do not acknowledge that their memory model is what it is, we do not have crucial operations such as "freeze" officially supported in compilers.
+At least for LLVM, that [might change though](http://www.cs.utah.edu/~regehr/papers/undef-pldi17.pdf).
+
 ## "What the hardware does" considered harmful
 
-Maybe the most important lesson to take away from this post is that "what the hardware does" is most of the time *irrelevant* when discussing what a Rust/C/C++ program does.
+Maybe the most important lesson to take away from this post is that "what the hardware does" is most of the time *irrelevant* when discussing what a Rust/C/C++ program does, unless you *already established that there is no undefined behavior*.
 Sure, hardware (well, [most hardware](https://devblogs.microsoft.com/oldnewthing/20040119-00/?p=41003)) does not have a notion of "uninitialized memory".
 But *the Rust program you wrote does not run on your hardware*.
 It runs on the Rust abstract machine, and that machine (which only exists in our minds) *does* have a notion of "uninitialized memory".
 The real, physical hardware that we end up running the compiled program on is a very efficient *but imprecise* implementation of this abstract machine, and all the rules that Rust has for undefined behavior work together to make sure that this imprecision is not visible for *well-behaved* (UB-free) programs.
 But for programs that do have UB, this "illusion" breaks down, and [anything is possible](https://raphlinus.github.io/programming/rust/2018/08/17/undefined-behavior.html).
 
+*Only* UB-free programs can be made sense of by looking at their assembly, but *whether* a program has UB is impossible to tell on that level.
+For that, you need to think in terms of the abstract machine.[^sanitizer]
+
+[^sanitizer]: This does imply that tools like valgrind, that work on the final assembly, can never reliably detect *all* UB.
+
 This does not just apply to uninitialized memory: for example, in x86 assembly, there is no difference between "relaxed" and "release"/"acquire"-style atomic memory accesses.
 But when writing Rust programs, even when writing Rust programs that you only intend to compile to x86, "what the hardware does" just does not matter.
 The Rust abstract machine *does* make a distinction between "relaxed" and "release"/"acquire", and your program will go wrong if you ignore that fact.