-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Use associated objects to attach contiguous array buffers to lazy ones #75148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use associated objects to attach contiguous array buffers to lazy ones #75148
Conversation
…ffers to lazy ones
@swift-ci please smoke test |
@swift-ci please test |
@swift-ci please Apple Silicon benchmark |
Benchmark results look fine so far, let's see if that stays true once I adopt it in CoW
|
@swift-ci please Apple Silicon benchmark |
Still doing ok. That honestly makes me wonder if we don't have benchmark coverage for this case, since I'd expect it to be better, worse, or both. |
@swift-ci please test |
Trying out some new benchmarks for this in #75215 |
@swift-ci please Apple Silicon benchmark |
@swift-ci please test |
(Note: this is still not fully ready, due to a thread safety issue) |
|
@swift-ci please smoke test |
@swift-ci please Apple Silicon benchmark |
@@ -51,12 +51,16 @@ internal struct _CocoaArrayWrapper: RandomAccessCollection { | |||
|
|||
@usableFromInline | |||
internal var endIndex: Int { | |||
return core.count | |||
@_effects(releasenone) get { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My claim is that any NSArray subclass that responds to -count or -objectAtIndex: by releasing something relevant is broken and will crash when used in ObjC code anyway
Fixed the code size issues! And adding the lock doesn't seem to have been particularly detrimental to performance
|
@swift-ci please smoke test |
@swift-ci please test |
@swift-ci please Apple Silicon benchmark |
@swift-ci please test |
|
||
@inlinable @_alwaysEmitIntoClient | ||
internal func setAssociatedBuffer(_ buffer: _ContiguousArrayBuffer<Element>) { | ||
let setter = unsafeBitCast(getSetAssociatedObjectPtr(), to: (@convention(c)( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This ridiculous dance is a way of tricking swiftc into calling objc_setAssociatedObject() directly without a) being able to import the definition, or b) going through the importer's id -> Any thunking
stdlib/public/core/ArrayBuffer.swift
Outdated
|
||
@inlinable @_alwaysEmitIntoClient | ||
internal func getAssociatedBuffer() -> _ContiguousArrayBuffer<Element>? { | ||
let getter = objc_getAssociatedObject as @convention(c)( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment below about objc_setAssociatedObject for what's going on here. It's almost the same trick but slightly less tangly because we don't have to deal with objc_associationPolicy
|
||
#endif | ||
|
||
#if !FOUNDATION_HELPER_FILE |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FoundationHelpers.mm does import the header, but this header can't import it, so we need to provide the decl only for non-FoundationHelpers.mm files. I hate this, suggestions welcome.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this section be moved to a new separate header?
stdlib/public/core/ArrayBuffer.swift
Outdated
internal func withUnsafeBufferPointer_nonNative<R>( | ||
_ body: (UnsafeBufferPointer<Element>) throws -> R | ||
) rethrows -> R { | ||
objc_sync_enter(_storage.objCInstance) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
objc_sync_enter is equivalent to @synchronized(), which gives us a way to get a lock here without declaring any storage for it, which in turn means we can do it from inlinable code in a back deployment friendly way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you expect this to usually be retrieved multiple per object, then this might benefit from double-checked locking. Get, return if set, otherwise lock and get again in case something else set it. The locking in the associated objects stuff saves you from needing to worry about memory barriers.
@swift-ci please test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks... I hesitate to say "good" so let's just say "correct," overall. A few little comments.
stdlib/public/core/ArrayBuffer.swift
Outdated
internal func withUnsafeBufferPointer_nonNative<R>( | ||
_ body: (UnsafeBufferPointer<Element>) throws -> R | ||
) rethrows -> R { | ||
objc_sync_enter(_storage.objCInstance) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you expect this to usually be retrieved multiple per object, then this might benefit from double-checked locking. Get, return if set, otherwise lock and get again in case something else set it. The locking in the associated objects stuff saves you from needing to worry about memory barriers.
|
||
#endif | ||
|
||
#if !FOUNDATION_HELPER_FILE |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this section be moved to a new separate header?
stdlib/public/core/ArrayBuffer.swift
Outdated
_storage.objCInstance, | ||
_ArrayBuffer.associationKey, | ||
buffer._storage, | ||
0o1401 //OBJC_ASSOCIATION_RETAIN |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OBJC_ASSOCIATION_RETAIN_NONATOMIC
will save you a retain/autorelease when getting the value. The atomic version just protects you from a concurrent set replacing the object while you're retrieving it, and that won't happen here. I expect the performance difference would be pretty small, though, since the autorelease will be elided.
stdlib/public/core/ArrayBuffer.swift
Outdated
setAssociatedBuffer(unwrapped) | ||
} | ||
defer { _fixLifetime(unwrapped) } | ||
objc_sync_exit(_storage.objCInstance) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty sure this is true but just checking: this is definitely 100% sure always going to be the exact same pointer that you got from _storage.objCInstance
above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm also pretty sure it's true, but there's no reason not to be paranoid and stash it in a local
stdlib/public/core/ArrayBuffer.swift
Outdated
@@ -189,6 +189,10 @@ extension _ArrayBuffer { | |||
internal __consuming func _consumeAndCreateNew( | |||
bufferIsUnique: Bool, minimumCapacity: Int, growForAppend: Bool | |||
) -> _ArrayBuffer { | |||
if !_isNative && capacity >= minimumCapacity, | |||
let associatedBuffer = getAssociatedBuffer() { | |||
return _ArrayBuffer(_buffer: associatedBuffer, shiftedToStartIndex: 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not look right -- IIUC this function needs to return a uniquely referenced buffer, which (again IIUC) this won't be
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh interesting point. It will become uniquely referenced the moment it replaces the old buffer (because deallocating the old buffer will remove the association). Is that sufficient?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm no it is not, because the old buffer might have an extra reference from another variable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed a radar to track figuring out how to do this safely, going to just rip it out for now
@swift-ci please test |
@swift-ci please Apple Silicon benchmark |
stdlib/public/core/ArrayBuffer.swift
Outdated
to: (@convention(c)( | ||
AnyObject, | ||
UnsafeRawPointer | ||
) -> Unmanaged<AnyObject>?).self |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using Unmanaged here prevents swiftc from generating pointless calls to objc_retainAutoreleasedReturnValue
@mikeash 's suggestions worked very nicely |
@swift-ci please test |
@swift-ci please test |
@swift-ci please test |
This uses ObjC runtime trickery to allow us to get an inner pointer with lifetime bounded to the array buffer from every array, rather than just from native ones
Fixes rdar://132124808