What is the correct way to print address for generic struct Set, to understand the implication of copy-on-write behavior?

Viewed 131

This is the way for me to print out the memory address of Array, to understand the implication of copy-on-write behavior of struct.

struct Array

func print(address o: UnsafeRawPointer ) {
    print(String(format: "%p", Int(bitPattern: o)))
}

var x = [1,2,3]
var y = x

// 0x600002654260
print(address: x)
// 0x600002654260   // x & y pointing to same address.
print(address: y)

x.append(4)

// 0x6000010504d0
print(address: x)   // Address changed after mutation, to avoid sharing same copy as y
// 0x600002654260
print(address: y)

x.append(5)

// 0x6000010504d0
print(address: x)   // Address remained after mutation
// 0x600002654260
print(address: y)

However, when I want to do the same study on generic Set, the previous way is no longer workable.

Generic struct Set

func print(address o: UnsafeRawPointer ) {
    print(String(format: "%p", Int(bitPattern: o)))
}

var x : Set = [1,2,3]
var y = x

print(address: x)
print(address: y)

x.insert(4)

print(address: x)
print(address: y)

x.insert(5)

print(address: x)
print(address: y)

I am getting the following error when calling print

cannot convert value of type 'Set' to expected argument type 'UnsafeRawPointer'


I had tried withUnsafePointer. But I suspect withUnsafePointer is not giving correct result. I am expecting both x and y should be pointing to the same address.

var x : Set = [1,2,3]
var y = x

// 0x000000010467c090
withUnsafePointer(to: &x) {
    print("\($0)")
}

// 0x000000010467c098
withUnsafePointer(to: &y) {
    print("\($0)")
}

What is the correct way to print address for generic struct Set, to understand the implication of copy-on-write behavior?

1 Answers

First, note that it is not the struct instances of Array<T> and Set<T> themselves that are copy-on-write. It is their backing stores that are. If you are confused by the fact that arrays and sets have backing stores, see this answer for a detailed description of how an array is structured. Sets are in a similar way.

Essentially, both Array and Set hold a reference to some reference type object (the backing store). If you look at the standard library source code, you can find __ContiguousArrayStorage and __RawSetStorage respectively, both of which are classes. It is this object that holds the actual elements in the collection, and it is this object that has COW behaviour. The Array and Set structs themselves behave like normal structs. After you do

var y = x

There are two Set structs in memory as you would expect, but they refer to the same backing store. withUnsafePointer gets the addresses of the two structs, which are of course different.

It's only when you modify one of them, that a new backing store object is created, and all the elements in the collection copied into the new backing store.

When you call print(address:) with an array, the array-to-pointer conversion actually gives you the pointer to the address of the backing store object, similar to how Array<T>.withUnsafeBufferPointer or Array<T>.withUnsafeBytes behave. Sets, on the other hand, don't have such a public API that gives you such a pointer.

You can use external tools to see the effect of COW though. A simple way is to use the Allocations instrument in Instruments.app to count how many set backing stores are allocated. Without any insert calls, there is only one allocation:

enter image description here

With one or two x.insert calls, there would be two allocations:

enter image description here

Related