Skip to content

[stdlib] performance optimizations in Array.replaceSubrange #66160

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

Merged
merged 7 commits into from
Mar 1, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 31 additions & 62 deletions stdlib/public/core/ArrayBufferProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,75 +149,44 @@ extension _ArrayBufferProtocol {
elementsOf newValues: __owned C
) where C: Collection, C.Element == Element {
_internalInvariant(startIndex == 0, "_SliceBuffer should override this function.")
let oldCount = self.count
let elements = self.firstElementAddress

// erase all the elements we're replacing to create a hole
let holeStart = elements + subrange.lowerBound
let holeEnd = holeStart + newCount
let eraseCount = subrange.count
holeStart.deinitialize(count: eraseCount)

let growth = newCount - eraseCount
// This check will prevent storing a 0 count to the empty array singleton.
if growth != 0 {
self.count = oldCount + growth
}

let elements = self.subscriptBaseAddress
let oldTailIndex = subrange.upperBound
let oldTailStart = elements + oldTailIndex
let newTailIndex = oldTailIndex + growth
let newTailStart = oldTailStart + growth
let tailCount = oldCount - subrange.upperBound

if growth > 0 {
// Slide the tail part of the buffer forwards, in reverse order
// so as not to self-clobber.
newTailStart.moveInitialize(from: oldTailStart, count: tailCount)

// Update the original subrange
var i = newValues.startIndex
for j in subrange {
elements[j] = newValues[i]
newValues.formIndex(after: &i)
}
// Initialize the hole left by sliding the tail forward
for j in oldTailIndex..<newTailIndex {
(elements + j).initialize(to: newValues[i])
newValues.formIndex(after: &i)
}
_expectEnd(of: newValues, is: i)
if growth != 0 {
let tailStart = elements + subrange.upperBound
let tailCount = self.count - subrange.upperBound
holeEnd.moveInitialize(from: tailStart, count: tailCount)
self.count += growth
}
else { // We're not growing the buffer
// Assign all the new elements into the start of the subrange
var i = subrange.lowerBound
var j = newValues.startIndex
for _ in 0..<newCount {
elements[i] = newValues[j]
i += 1
newValues.formIndex(after: &j)
}
_expectEnd(of: newValues, is: j)

// If the size didn't change, we're done.
if growth == 0 {
return
}

// Move the tail backward to cover the shrinkage.
let shrinkage = -growth
if tailCount > shrinkage { // If the tail length exceeds the shrinkage

// Update the rest of the replaced range with the first
// part of the tail.
newTailStart.moveUpdate(from: oldTailStart, count: shrinkage)

// Slide the rest of the tail back
oldTailStart.moveInitialize(
from: oldTailStart + shrinkage, count: tailCount - shrinkage)
// don't use UnsafeMutableBufferPointer.initialize(fromContentsOf:)
// since it behaves differently on collections that misreport count,
// and breaks validation tests for those usecases / potentially
// breaks ABI guarantees.
if newCount > 0 {
let done: Void? = newValues.withContiguousStorageIfAvailable {
_precondition(
$0.count == newCount,
"invalid Collection: count differed in successive traversals"
)
holeStart.initialize(from: $0.baseAddress!, count: newCount)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's so annoying that withContiguousStorageIfAvailable is allowed to pass a buffer with a nil baseAddress rather than just returning nil without executing the closure in that case. Otherwise we could use .unsafelyUnwrappedUnchecked here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Actually what IS supposed to happen in that case? Should it just be the same as if newValues was empty?)

Copy link
Contributor Author

@oxy oxy Jun 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, it says that if there is no contiguous representation, it should ignore body and just return nil?
and in this case, newValues.count / newCount should be the same (and from above, > 0) for a well-implemented collection?

I'm not sure if its worth throwing in an extra assert for $0.count == newCount, or removing ! for .unsafelyUnwrappedUnchecked either.

}
else { // Tail fits within erased elements
// Update the start of the replaced range with the tail
newTailStart.moveUpdate(from: oldTailStart, count: tailCount)

// Destroy elements remaining after the tail in subrange
(newTailStart + tailCount).deinitialize(
count: shrinkage - tailCount)
if done == nil {
var place = holeStart
var i = newValues.startIndex
while place < holeEnd {
place.initialize(to: newValues[i])
place += 1
newValues.formIndex(after: &i)
}
_expectEnd(of: newValues, is: i)
}
}
}
Expand Down