bindMemory and invalidation of existing pointers

Hello. I am wondering about the reasoning behind invalidating existing typed pointers whent bindMemory is used. Why using old pointers that were bound to a different type is undefined behaviour? What could theoreticly go wrong? Any examples? (Considering we are dealing with primitive C compatible data types/structs)
Answered by Developer Tools Engineer in 616751022

Part 1

One really powerful optimization that the compiler wants to do whenever it can is removing accesses to memory. For example, consider this code:

Code Block swift
func setPointerAndPrint(ptr1: UnsafeMutablePointer<Int>) {
   ptr1.pointee = 1
   print(ptr1.pointee)
}

The compiler can see very clearly that, by the time we reach print(ptr1.pointee), its value could only possibly be 1, so it can turn that line into print(1) instead without changing your program’s behavior. This is a small savings for this particular bit of code, but in the general case, this kind of optimization can enable much more powerful ones. For example, if this rule turns a guard !ptr1.pointee.isMultiple(of: 2) into guard !1.isMultiple(of: 2), then the compiler could determine that the check will never fail and delete the whole guard statement. Small, individual optimizations like this can snowball into very large performance differences.

But this only works if the compiler can tell that none of the code between those lines could change ptr1.pointee. For example, if the code looked like this:

Code Block swift
func setPointerAndPrint(ptr1: UnsafeMutablePointer<Int>) {
   ptr1.pointee = 1
   someFunctionInAnotherModule()
   print(ptr1.pointee)
}

Then Swift can’t turn print(ptr1.pointee) into print(1), because it doesn’t know what someFunctionInAnotherModule() might do—it could set ptr1.pointee to a different value. So it doesn’t remove that memory access.



One especially vexing aspect of this problem is called “aliasing”. Aliasing is when two pointers point to the same memory, so a write to one pointer will change the value read from the other. Maybe 95% of the time, two pointers involved in the same operation don’t alias each other—but if the compiler simply assumes that they don’t alias, the code it generates will behave incorrectly in the remaining 5% of cases.

For example, consider this function:

Code Block swift
func setPointersAndPrint(ptr1: UnsafeMutablePointer<Int>, ptr2: UnsafeMutablePointer<Int>) {
   ptr1.pointee = 1
   ptr2.pointee = 2
   print(ptr1.pointee)
}

Almost all of the time, the write to ptr2.pointee won’t affect ptr1.pointee, so the compiler would like to convert print(ptr1.pointee) into print(1). But you could pass the same pointer to both parameters, and then that change would produce an incorrect result, so the compiler can’t perform that optimization. It’s rather irritating to give up that performance improvement.



But what about this function?

Code Block swift
func setPointersAndPrint(ptr1: UnsafeMutablePointer<Int>, ptr2: UnsafeMutablePointer<UInt>) {
   ptr1.pointee = 1
   ptr2.pointee = 2
   print(ptr1.pointee)
}

It is at least theoretically possible to make ptr1 and ptr2 point to the same address, but it doesn’t really make sense to do that, because the two are of different types! Maybe there’s a 0.001% situation where someone is intentionally doing something really weird, but we’d really like that speedup in the remaining 99.999% of situations, and we don’t want to lose such a big win for such a niche situation. 5% is us being careful; 0.001% is us sulking in our tents.

So we don’t sulk. Instead, we say that, if two typed pointers do not have compatible pointee types, they are not allowed to alias, and allow the Swift optimizer to assume this.



To be continued...
Accepted Answer

Part 1

One really powerful optimization that the compiler wants to do whenever it can is removing accesses to memory. For example, consider this code:

Code Block swift
func setPointerAndPrint(ptr1: UnsafeMutablePointer<Int>) {
   ptr1.pointee = 1
   print(ptr1.pointee)
}

The compiler can see very clearly that, by the time we reach print(ptr1.pointee), its value could only possibly be 1, so it can turn that line into print(1) instead without changing your program’s behavior. This is a small savings for this particular bit of code, but in the general case, this kind of optimization can enable much more powerful ones. For example, if this rule turns a guard !ptr1.pointee.isMultiple(of: 2) into guard !1.isMultiple(of: 2), then the compiler could determine that the check will never fail and delete the whole guard statement. Small, individual optimizations like this can snowball into very large performance differences.

But this only works if the compiler can tell that none of the code between those lines could change ptr1.pointee. For example, if the code looked like this:

Code Block swift
func setPointerAndPrint(ptr1: UnsafeMutablePointer<Int>) {
   ptr1.pointee = 1
   someFunctionInAnotherModule()
   print(ptr1.pointee)
}

Then Swift can’t turn print(ptr1.pointee) into print(1), because it doesn’t know what someFunctionInAnotherModule() might do—it could set ptr1.pointee to a different value. So it doesn’t remove that memory access.



One especially vexing aspect of this problem is called “aliasing”. Aliasing is when two pointers point to the same memory, so a write to one pointer will change the value read from the other. Maybe 95% of the time, two pointers involved in the same operation don’t alias each other—but if the compiler simply assumes that they don’t alias, the code it generates will behave incorrectly in the remaining 5% of cases.

For example, consider this function:

Code Block swift
func setPointersAndPrint(ptr1: UnsafeMutablePointer<Int>, ptr2: UnsafeMutablePointer<Int>) {
   ptr1.pointee = 1
   ptr2.pointee = 2
   print(ptr1.pointee)
}

Almost all of the time, the write to ptr2.pointee won’t affect ptr1.pointee, so the compiler would like to convert print(ptr1.pointee) into print(1). But you could pass the same pointer to both parameters, and then that change would produce an incorrect result, so the compiler can’t perform that optimization. It’s rather irritating to give up that performance improvement.



But what about this function?

Code Block swift
func setPointersAndPrint(ptr1: UnsafeMutablePointer<Int>, ptr2: UnsafeMutablePointer<UInt>) {
   ptr1.pointee = 1
   ptr2.pointee = 2
   print(ptr1.pointee)
}

It is at least theoretically possible to make ptr1 and ptr2 point to the same address, but it doesn’t really make sense to do that, because the two are of different types! Maybe there’s a 0.001% situation where someone is intentionally doing something really weird, but we’d really like that speedup in the remaining 99.999% of situations, and we don’t want to lose such a big win for such a niche situation. 5% is us being careful; 0.001% is us sulking in our tents.

So we don’t sulk. Instead, we say that, if two typed pointers do not have compatible pointee types, they are not allowed to alias, and allow the Swift optimizer to assume this.



To be continued...

Part 2

But it’s very difficult for people and tools to reason about a global rule like “two pointers of different types cannot point to the same address”. It’s just not actionable. How do you know if your code is correct?

So instead, we break that global rule down into a bunch of local rules which have the same effect as long as everyone follows them, like:
  1. Use assumingMemoryBound(to:) only when you know there is other code which has bound that memory to that type.

  2. During withMemoryRebound(to:capacity:_:), pointers to that memory using the original type can’t be used; after it finishes, pointers using the rebound type can’t be used.

  3. Use bindMemory(to:) only when any pointer to that memory that uses a different type should no longer be used.

Unlike the global rule, which gives you no guidance for how to actually make it happen, these rules are ones you at least have a hope of verifying by looking at your own code:
  1. “Okay, I can see that up here, this was a pointer to Foo, and then I turned it into an UnsafeRawPointer and it came out down here, so it’s safe to assumingMemoryBound(to: Foo.self) here.”

  2. “Okay, I can see the code in this closure and I know it doesn’t use the pointer I called withMemoryRebound(to:capacity:_:) on, so it’s correct.”

  3. “Okay, nothing should be using the memory in my allocator’s free heap, so it’s safe to bindMemory(to:) the type we want to allocate.”

When we say that “using bindMemory(to:) invalidates existing typed pointers”, that’s just another way to phrase rule 3—that you should only call bindMemory(to:) if any existing pointers with the old type will no longer be used.



So, I think that covers why the language has this rule: it helps to make it illegal to use two incompatibly-typed pointers that point to the same memory, which allows the optimizer to assume that incompatibly-typed pointers don’t affect each other, so it can generate faster code. 

So, what happens if you violate the rules? Well, anything (or nothing) could happen, but I can give you some hypothetical examples.

Remember this function?

Code Block swift
func setPointersAndPrint(ptr1: UnsafeMutablePointer<Int>, ptr2: UnsafeMutablePointer<UInt>) {
   ptr1.pointee = 1
   ptr2.pointee = 2
   print(ptr1.pointee)
}

It could print “1” instead of “2”, because it assumed that setting ptr2.pointee couldn’t affect ptr1.pointee.

Now, how about this function?

Code Block swift
func doScaryOperationSafely(ptr1: UnsafeMutablePointer<Int>, ptr2: UnsafeMutablePointer<UInt>) {
   print(“value was \(ptr1.pointee)”)
   ptr2.pointee = 2
   guard !ptr1.pointee.isMultiple(of: 2) else {
     print(“warning: almost melted down!”)
     return
   }
   doScaryOperationThatWillMeltDownIfThePointeeIsEven(ptr1)
}

It could use the value retrieved before ptr2.pointee was set in the guard statement, and therefore call doScaryOperationThatWillMeltDownIfThePointeeIsEven(_:) when the pointee is 2.

Really, the sky’s the limit on how bad it could get. Just as small optimizations can snowball into large performance improvements, small mis-optimizations can snowball into large behavior differences.
Thank you for this thorough and informative reply! This really made things clear.
This answer adds to the previous informative and correct answer in order to address this part of the question: "what could theoretically go wrong...considering we are dealing with primitive C compatible data types/structs"

Here's an example of why Swift needs strict pointer types, and why using bindMemory incorrectly on "primitive C compatible data types" will lead to incorrect program behavior, even if the Swift compiler was maximally conservative and did not perform any pointer-related optimization.

Building this code with for release (-O) will cause the precondition to trigger because the C compiler has optimized addTenTimes assuming that S1 * and S2 * do not alias:

file.h
Code Block
struct S1 {
int i1;
} S1;
struct S2 {
int i2;
} S2;
void addTenTimes(struct S1 *s1, const struct S2 *s2);


file.c
Code Block
void addTenTimes(struct S1 *s1, const struct S2 *s2) {
for (int i = 0; i < 10; ++i)
s1->i1 += s2->i2;
}


file.swift
Code Block
// Initialize S1.i1.
let s1Ptr = UnsafeMutablePointer<S1>.allocate(capacity: 1)
s1Ptr.pointee.i1 = 1
// Rebind S1 to S2--they are layout-compatible.
let s2Ptr = UnsafeRawPointer(s1Ptr).bindMemory(to: S2.self, capacity: 1)
// Call a C routine with aliased pointers.
addTenTimes(s1Ptr, s2Ptr);
precondition(s1Ptr.pointee.i1 == 1024)


There are other cases in which the C compiler cannot produce unexpected behavior, but the Swift compiler theoretically could. That doesn't mean the the current Swift compiler will produce unexpected behavior. There is some leeway between what the compiler is allowed to do vs. what the current compiler version does. Imagine running a sanitizer that enforces the undefined-behavior rule for typed pointer access:
Code Block
It is invalid to access a typed pointer in Swift when the accessed memory location is bound to a type that is incompatible with the pointer type.

If that hypothetical sanitizer passes when running all paths in your code, then some future compiler can do anything it is allowed to do within the rules and your program behavior will not change. That's a great property. Without such a sanitizer though, it is up to you to follow the simple rule stated above.

There are advantages to Swift being even more strict than C in this regard:
  • A single, consistent rule means that Swift programmers won't be misled when reading code that appears to break the rule, but actually takes advantage of a special case in the language.

  • Swift pointer semantics only depend on the nominal pointer type, not the generic pointee type. So code that works on UnsafePointer<T> has the same pointer safety semantics for all T.

  • Overall program type safety is improved when pointers types are fully enforced. A pointer type mismatch is more likely a legitimate programming error rather than a deliberate attempt to reinterpret a type.

  • Swift pointers won't undermine Swift's stronger enforcement of non-pointer types. Unlike C, Swift checks signed/unsigned integer conversions on values. C considers types qualified by different signed or unsigned qualifiers to be pointer-compatible, while Swift does not. If Swift allowed those types to be pointer-compatible, then programs could accidentally bypass Swift's normal type conversion checks just by using pointers.

bindMemory and invalidation of existing pointers
 
 
Q