Swift ARC Weak/Unowned performance

It looks to me like $0 is retained when returned from the closure:

return _a!._withUnsafeGuaranteedRef { $0 }

Making it identical to this in terms of reference counting:

return _a!.takeUnretainedValue()

Although the takeUnretainedValue version emits an "unoptimizable" retain, while the withUnsafeGuaranteedRef version emits an regular "optimizable" retain.

2 Likes

Do there currently exist both of these APIs on Unmanaged?

  • Take and retain.
  • Take and do not retain.

If so, what are they each called?

As a user, those are the semantics I care about: sometimes I have a value which needs to be retained when taken, and sometimes I have a value which needs to not be retained when taken.

With the current confusing names, I am not sure if those things are possible, nor if so how they are spelled.

3 Likes

The APIs on Unmanaged are "take at +0", which will do a retain in practice, and "take at +1", which will take ownership of an existing retain. There's nothing that says "borrow for now without retaining and without taking ownership of a retain", which is why _withUnsafeGuaranteedRef is useful.


I'm still not sure why takeUnretainedValue has an "unoptimizable" retain. @lorentey's logic makes sense to me except for the conclusion:

The conclusion I take is that the compiler will insert a retain when a release might happen. If I just do this:

final class IntWrapper {
  var value: Int = 0
}

func test(_ obj: Unmanaged<IntWrapper>) {
  let wrapper = obj.takeUnretainedValue()
  let value = wrapper.value
  print(value)
}

Then there's no reason to perform an actual retain, because accessing a known-stored property can't cause anything to get deinitialized. The programmer-provided assumption that obj is valid at the time of takeUnretainedValue hasn't been invalidated yet.

Of course, _withUnsafeGuaranteedRef is still useful, because as soon as you do anything the compiler can't see (or that definitely could cause deinitialization), that assumption is invalidated. So in practice I wouldn't expect this change to help too many programs. But it definitely could help, and given the documentation for Unmanaged.takeUnretainedValue it's a valid optimization to make.

1 Like

Just to make sure I'm understanding the extra retain, doing the following explicitly should avoid the extra retain, right?
unowned(unsafe) let wrapper = obj.takeUnretainedValue()

With return _a!._withUnsafeGuaranteedRef { $0 } we end up with:

  %11 = struct_extract %10 : $Unmanaged<A>, #Unmanaged._value // user: %12
  %12 = unmanaged_to_ref %11 : $@sil_unmanaged A to $A // users: %15, %14, %13
  strong_retain %12 : $A                          // id: %14

With return _a!.takeUnretainedValue, we end up with:

  %11 = struct_extract %10 : $Unmanaged<A>, #Unmanaged._value // user: %12
  %12 = strong_copy_unmanaged_value %11 : $@sil_unmanaged A // user: %13
  return %12 : $A                                 // id: %13

SIL.rst says of strong_copy_unmanaged_value:

This is intended to be used semantically as a "conversion" like
instruction from unmanaged to strong and thus should never be
removed by the optimizer.

@Michael_Gottesman added this recently:

I don't know what bug was fixed by the change. No bug is referenced and I don't see anything revealing in the tests.

Do we have any idea if unowned(unsafe) performance has been fixed?

From reading this: Optimization Tips

Note, Unmanaged<T>._withUnsafeGuaranteedRef is not a public API and will go away in the future. Therefore, don't use it in code that you can not change in the future.

I'm shipping a networking library, and I can't use _withUnsafeGuaranteedRef if it's going to eventually be removed and cause customer code in shipped applications to start crashing when users upgrade their devices to a new version of Swift. Am I correct in my assumption that this is going to be the case?

The Swift runtime libraries are ABI-stable, so we wouldn't intentionally make a change that could break existing binaries. We also take source stability seriously. If we were to deprecate that API, we would do so in such a way that wouldn't affect existing shipped binaries, and would hopefully provide a warning when you try to compile your code with a new SDK that introduced the deprecation.

4 Likes

Thank you for the clarification @Joe_Groff! That's what I had hoped to hear. I assumed that level of care was put in, but after reading that line I quoted above, I wanted to make sure before shooting myself in the foot here. I appreciate the quick response!