Doesn't Swift borrow immutable structs by default?

I have just found out that you can pass Copyable structs into a function that declares the parameter as borrowing. Which is pretty cool, optimizations and stuff..

However:

  • struct is a value type and we are taught that these get copied around in function calls
  • if your passed instance is immutable, there actually is zero reason for compiler to copy the data, when it can just copy a reference to that instance
  • hence, for all immutable structs, there should never be any copying because it's just not necessary

I also heard something sometime that Swift "sometimes" doesn't copy the struct, but rather passes a references - this I heard through the grapevine back in the Swift 4 times.

So, this reignites a question in my head:

  • does swift copy immutable structs?
  • if it does, why? could've just passed an immutable ref
  • if it does, am I optimizing anything by using borrowing in all applicable contexts?
1 Like

For small structs copying a reference is more expensive than copying the struct itself

https://twiceasmuch.space/

5 Likes

But where is this margin of "small"?

And I'm obviously not talking about scalars shorter than a machine word - I am talking about a structure with 7 other 32-64 byte struct fields, as well as an Array and a Dictionary.

Somebody more knowledgeable will likely be able to correct me, but my high-level understanding is as follows: a value will be copied if it needs to be put into some new memory location while also keeping it in the old, for instance when:

  • passing it into an init
  • assigning to a property
  • capturing into an escaping closure
  • appending to an array

— and so on. SIL actually gives pretty definitive answers to this: for example, MyStruct.init(arr: [Int]) gets the SIL signature (@owned Array<Int>, @inout MyStruct) -> (), i.e. it expects an owned instance of [Int], so the caller has to either ensure that the value is not used any further or create a separate copy.

As another example, when compiling

func foo(arr: [Int]) {
    var bigarr = [[Int]]()
    bigarr.append(arr)
}

SIL generates a declaration (for a thunk of sorts, I guess..?) with (Int, @owned Array<Int>, @inout Array<Array<Int>>) -> (), i.e. the function will want to consume the [Int] passed into foo. Visit Compiler Explorer and search for "append" in the output.

In the rest of the typical cases when there's no need to populate a new memory region, the value will be passed as @guaranteed:

func foo(arr: [Int]) -> Int {
    return arr[0]
}

is (@guaranteed Array<Int>) -> Int.

I'm not a SIL wizard, but this looks like a good approach to figure out this question on a case-to-case basis. It also shows that rewriting the above to func foo(arr: borrowing [Int]) -> Int does nothing.

3 Likes

ohhhh that's exactly the kind of answer I wanted to see, thank you

I beleive @Joe_Groff has some clues here… you might want to keep looking around for more details.

3 Likes

Borrowing is the default ownership convention for most functions, but borrowing is orthogonal to whether the value is physically copied or passed by reference at the machine calling convention level. Small value types such as reference-counted pointers, integer file descriptors, etc. represent the same resource regardless of where the value is in memory (or if it's in memory at all), so borrowing them is done by passing their representation by value, but without retaining or releasing the pointer. Values of types that either do have a significant address (such as C++ types, or weak references), or are larger than a given threshold (four pointers's size), or are of unknown size (such as unspecialized generics) are passed by address.

20 Likes

So are there any typical cases where passing copyable value types can be optimized with borrowing?

Or, for copyable value types, is the borrowing modifier rather about "don't allow consumption" / "don't allow implicit copy".

Should I establish a specific mental model for borrowing for copyable value types?