Tuples support copy-on-write?

This site, What is Copy on Write (CoW) in Swift? | by Gaye Uğur | Medium), suggests that tuples support copy on write. This doesn't sound right to me (I was under the impression that COW is something you have to actively implement yourself [or has been implemented in the StdLib], whereas tuples seem like something baked into the language.) Is it true that tuples support COW? Or is this web page mistaken? (Hey! It's on the Internet, it must be true, right?)
Thanks

Nothing in Swift automatically supports copy on write, it's manually added to type that need it, like the default collection types. CoW types in tuples should still CoW as part of copies of the tuple.

2 Likes

Obviously, if one of the fields is a COW type, that field will be copied-on-write. I'm talking about something simple like

typealias Example = (Int, Int, Int). 
var a :Example(1,2,3)
var b = a

Does it immediately copy all of a to b, or will it defer until one of them changes a member? My thought was the former.

In an example this simple the optimizer will likely delete the copy entirely, but assuming it doesn’t, it will eagerly copy rather than on write.

Also note that structs are not guaranteed to be contiguous in memory, so if say, only one field is actually used, the optimizer is allowed to just not store the others at all.

6 Likes

OK, thanks, that's what I wanted to verify.

just to clarify here @David_Smith – did you mean structs or tuples (or both)?

The second code block (and probably the first one but I didn’t check because it’s actually a screenshot) does not compile in Godbolt because you can’t cast an arbitrary value type to AnyObject. The whole thing smells a bit of ChatGPT to me so I wouldn’t take it too seriously.

1 Like

Are tuples not guaranteed to be contiguous in memory? While it seems reasonable that you should not expect this, I read so many posts that claim this is one advantage of tuples over structs. I've even found code that provides an index into a tuple (like an array), assuming the tuple has homogeneous types:

struct myArray4s
{
   private var ma4Storage: (Int, Int, Int, Int)
   
   init( _ i0:Int, _ i1:Int, _ i2:Int, _ i3:Int )
   {
      ma4Storage.0 = i0
      ma4Storage.1 = i1
      ma4Storage.2 = i2
      ma4Storage.3 = i3
   }
   
   var count :Int
   {
      return 4
   }
   
   subscript( index:Int )->Int
   {
      mutating get
      {
         withUnsafeMutablePointer(to: &self.ma4Storage)
         {  ma4Ptr in
            ma4Ptr.withMemoryRebound(to: Int.self, capacity: 4)
            {  ma4Array in
               return ma4Array[index]
            }
         } // end withUnsafeMutablePointer
      } // end getter
      
      set( newValue )
      {
         withUnsafeMutablePointer(to: &ma4Storage)
         {  ma4Ptr in
            ma4Ptr.withMemoryRebound(to: Int.self, capacity: 4)
            {  ma4Array in
               ma4Array[index] = newValue
            }
         } // end withUnsafeMutablePointer
      } // end setter
   } // end subscript
} // end myArray4s

var my4 = myArray4s( 0, 1, 2, 3 )

print( "Struct version: ", terminator:"" )
for i in 0..<my4.count
{
   print( my4[i], terminator:" " )
}
print()

my4[2] = 15

print( "Struct version2: ", terminator:"" )
for i in 0..<my4.count
{
   print( my4[i], terminator:" " )
}
print()
print( my4 )

The output from this is:

Struct version: 0 1 2 3 
Struct version2: 0 1 15 3 
myArray4s(ma4Storage: (0, 1, 15, 3))

Which suggests the tuple elements are stored contiguously in memory. Of course, the fact that this program demonstrates this "feature" today is no guarantee it will continue to work tomorrow; but I am wondering if contiguous storage for tuple is something Swift guarantees, or if this is just a coincidence.

Though note: Guarantees of Tuples as Fixed Sized (stack allocated) Arrays

1 Like

John's point is the relevant bit here:

Tuples are guaranteed to use a standard C-style layout wherever that layout is ABI-observable, e.g. when you construct an UnsafePointer to one.

Calling withUnsafeMutablePointer causes Swift to materialize a temporary contiguous addressable version, which may or may not exist before or after.

6 Likes

Exactly what David said. If the memory layout is observed by the program, the tuple is guaranteed to be laid out contiguously in memory. If it isn't, then the compiler is free to never build the tuple at all, or keep it in registers, or lay it out in some other manner.

2 Likes

And it's free to construct the contiguous tuple from scratch before the tuple is observed, and decompose it again after the observation is finished.

var elements = (0, 1, 2, 3, 4)
// contiguous storage may or may not exist here.
print(elements.1, elements.3)
withUnsafeMutablePointer(to:  elements) { arr in
   // contiguous storage exists here
   for i in 0..<5 {
      (arr + i).pointee *= 2
   }
}
// contiguous storage may not exist here.
print(elements.0, elements.2, elements.4)

And the optimizer may still optimize the whole thing into:

print(1, 3)
print(0, 4, 8)

Or anything in between.

1 Like