Search code examples
swiftcopy-on-write

Test Copy-on-write in swift


 import Foundation

 func address(o:UnsafeRawPointer) -> Int {
     return Int(bitPattern: o)
 }
 var originArray = [1,2,3]
 var firstArray = originArray


//q.append(4)
print(NSString.init(format: "originArray:%p", address(o: &originArray)))
print(NSString.init(format: "firstArray:%p", address(o: &firstArray)))

Debug Log: originArray:0x100b087b0 firstArray:0x100b088c0

Above, it's my test code.I think that I don't modify originArray which like appending or reduceing element. they should point same address.but why deference


Solution

  • Your code is printing the addresses of the array buffers (Array is a special case when passing a value to a pointer parameter). However, in Swift 3, the compiler assumed that the presence of the & operator meant that the buffer was being passed as mutable memory, so (unnecessarily) made it unique (by copying) before passing its pointer value, despite that pointer value being passed as an UnsafeRawPointer. That's why you see different addresses.

    If you remove the & operator and pass the arrays directly:

    func address(_ p: UnsafeRawPointer) {
        print(p)
    }
    
    var originArray = [1, 2, 3]
    var firstArray = originArray
    
    address(originArray) // 0x00000000016e71c0
    address(firstArray)  // 0x00000000016e71c0
    

    You'll now get the same addresses, as the compiler now assumes that address(_:) will not modify the memory of the buffers passed, as they're being passed to an UnsafeRawPointer parameter.

    In Swift 4, this inconsistency is fixed, and the compiler no longer makes the buffer unique before passing its pointer values to an UnsafeRawPointer parameter, even when using the & operator, so your code exhibits expected behaviour.

    Although, it's worth noting that the above method isn't guaranteed to produce stable pointer values when passing the same array to multiple pointer parameters.

    From the Swift blog post "Interacting with C Pointers":

    Even if you pass the same variable, array, or string as multiple pointer arguments, you could receive a different pointer each time.

    I believe this guarantee cannot be met for arrays in two cases (there may be more):

    1. If the array is viewing elements in non-contiguous storage

      Swift's Array can view elements in non-contiguous storage, for example when it is wrapping an NSArray. In such a case, when passing it to a pointer parameter, a new contiguous buffer will have to be created, therefore giving you a different pointer value.

    2. If the buffer is non-uniquely referenced when passed as mutable memory

      As mentioned earlier, when passing an array to a mutable pointer parameter, its buffer will first be made unique in order to preserve value semantics, as it's assumed the function will perform a mutation of the buffer.

      Therefore, if the buffer was copied, you'll get a different pointer value to if you had passed the array to an immutable pointer parameter.

    Although neither of these two points are applicable in the example you give, it's worth bearing in mind that the compiler still doesn't guarantee you stable pointer values to the array's buffer when passing to pointer parameters.

    For results that are guaranteed to be reliable, you should use the withUnsafeBytes(_:) method on a ContiguousArray:

    var originArray: ContiguousArray = [1, 2, 3]
    var firstArray = originArray
    
    originArray.withUnsafeBytes { print($0.baseAddress!) } // 0x0000000102829550
    firstArray.withUnsafeBytes { print($0.baseAddress!) }  // 0x0000000102829550
    

    This is because withUnsafeBytes(_:) is documented as accepting:

    A closure with an UnsafeRawBufferPointer parameter that points to the contiguous storage for the array. If no such storage exists, it is created.

    And ContiguousArray guarantees that:

    [it] always stores its elements in a contiguous region of memory

    And just like Array, ContiguousArray uses copy-on-write in order to have value semantics, so you can still use it to check when the array's buffer is copied upon a mutation taking place:

    var originArray: ContiguousArray = [1, 2, 3]
    var firstArray = originArray
    
    originArray.withUnsafeBytes { print($0.baseAddress!) } // 0x0000000103103eb0
    firstArray.withUnsafeBytes { print($0.baseAddress!) }  // 0x0000000103103eb0
    
    firstArray[0] = 4
    
    originArray.withUnsafeBytes { print($0.baseAddress!) } // 0x0000000103103eb0
    firstArray.withUnsafeBytes { print($0.baseAddress!) }  // 0x0000000100e764d0