-
Notifications
You must be signed in to change notification settings - Fork 442
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
Add partitioned(by:)
#152
Add partitioned(by:)
#152
Conversation
I made the closure |
I ran some benchmarks using the awesome swift-collections-benchmark package, as suggested by @timvermeulen: The output does confirm that using Using the I was initially surprised Using a slighty more expensive closure yielded these results: Benchmarking code/detailsSimple closure test:benchmark.addSimple(
title: "Filter × 2",
input: [Int].self
) { input in
blackHole(input.filter({ $0.isMultiple(of: 3) }))
blackHole(input.filter({ !$0.isMultiple(of: 3) }))
}
benchmark.addSimple(
title: "Partitioned (Sequence)",
input: [Int].self
) { input in
blackHole(input._partitioned({ $0.isMultiple(of: 3) }))
}
benchmark.addSimple(
title: "Partitioned (Collection)",
input: [Int].self
) { input in
blackHole(input.partitioned({ $0.isMultiple(of: 3) }))
} More expensive closure test: let multiples: [Int] = [1, 3, 5, 7]
benchmark.addSimple(
title: "Filter × 2",
input: [Int].self
) { input in
blackHole(input.__filter({ int in multiples.allSatisfy({ int.isMultiple(of: $0) }) }))
blackHole(input.__filter({ int in !multiples.allSatisfy({ int.isMultiple(of: $0) }) }))
}
benchmark.addSimple(
title: "Partitioned (Sequence)",
input: [Int].self
) { input in
blackHole(input._partitioned({ int in multiples.allSatisfy({ int.isMultiple(of: $0) }) }))
}
benchmark.addSimple(
title: "Partitioned (Collection)",
input: [Int].self
) { input in
blackHole(input.partitioned({ int in multiples.allSatisfy({ int.isMultiple(of: $0) }) }))
} All tests run on iMac Pro 3.2 GHz 8-Core Intel Xeon W; 32 GB 2666 MHz DDR4; macOS 11.3 (20E232); Apple Swift version 5.4.2 (swiftlang-1205.0.28.2 clang-1205.0.19.57) |
…implementation This constant was determined using benchmarking. More information: apple#152 (comment)
…implementation This constant was determined using benchmarking. More information: apple#152 (comment)
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.
Those graphs look good! It's nice to see that the added complexity seems to be worth it for most sizes. I think it'd be useful to benchmark another version that returns a (ArraySlice, ArraySlice)
pair (or even (ArraySlice, ReversedCollection<ArraySlice>)
) just so we can see how much performance we're missing out on by allocating two new arrays.
…implementation This constant was determined using benchmarking. More information: apple#152 (comment)
Codeextension Collection {
@inlinable
public func partitionedA(
_ belongsInSecondCollection: (Element) throws -> Bool
) rethrows -> ([Element], [Element]) {
guard !self.isEmpty else {
return ([], [])
}
// Since `RandomAccessCollection`s have known sizes (access to `count` is
// constant time, O(1)), we can allocate one array of size `self.count`,
// then insert items at the beginning or end of that contiguous block. This
// way, we don’t have to do any dynamic array resizing. Since we insert the
// right elements on the right side in reverse order, we need to reverse
// them back to the original order at the end.
let count = self.count
// Inside of the `initializer` closure, we set what the actual mid-point is.
// We will use this to partitioned the single array into two in constant time.
var midPoint: Int = 0
let elements = try [Element](
unsafeUninitializedCapacity: count,
initializingWith: { buffer, initializedCount in
var lhs = buffer.baseAddress!
var rhs = lhs + buffer.count
do {
for element in self {
if try belongsInSecondCollection(element) {
rhs -= 1
rhs.initialize(to: element)
} else {
lhs.initialize(to: element)
lhs += 1
}
}
let rhsIndex = rhs - buffer.baseAddress!
buffer[rhsIndex...].reverse()
initializedCount = buffer.count
midPoint = rhsIndex
} catch {
let lhsCount = lhs - buffer.baseAddress!
let rhsCount = (buffer.baseAddress! + buffer.count) - rhs
buffer.baseAddress!.deinitialize(count: lhsCount)
rhs.deinitialize(count: rhsCount)
throw error
}
})
let lhs = elements[..<midPoint]
let rhs = elements[midPoint...]
return (
Array(lhs),
Array(rhs)
)
}
}
extension Collection {
@inlinable
public func partitionedB(
_ belongsInSecondCollection: (Element) throws -> Bool
) rethrows -> (ArraySlice<Element>, ArraySlice<Element>) {
guard !self.isEmpty else {
return ([], [])
}
// Since `RandomAccessCollection`s have known sizes (access to `count` is
// constant time, O(1)), we can allocate one array of size `self.count`,
// then insert items at the beginning or end of that contiguous block. This
// way, we don’t have to do any dynamic array resizing. Since we insert the
// right elements on the right side in reverse order, we need to reverse
// them back to the original order at the end.
let count = self.count
// Inside of the `initializer` closure, we set what the actual mid-point is.
// We will use this to partitioned the single array into two in constant time.
var midPoint: Int = 0
let elements = try [Element](
unsafeUninitializedCapacity: count,
initializingWith: { buffer, initializedCount in
var lhs = buffer.baseAddress!
var rhs = lhs + buffer.count
do {
for element in self {
if try belongsInSecondCollection(element) {
rhs -= 1
rhs.initialize(to: element)
} else {
lhs.initialize(to: element)
lhs += 1
}
}
let rhsIndex = rhs - buffer.baseAddress!
buffer[rhsIndex...].reverse()
initializedCount = buffer.count
midPoint = rhsIndex
} catch {
let lhsCount = lhs - buffer.baseAddress!
let rhsCount = (buffer.baseAddress! + buffer.count) - rhs
buffer.baseAddress!.deinitialize(count: lhsCount)
rhs.deinitialize(count: rhsCount)
throw error
}
})
let lhs = elements[..<midPoint]
let rhs = elements[midPoint...]
return (lhs, rhs)
}
}
extension Collection {
@inlinable
public func partitionedC(
_ belongsInSecondCollection: (Element) throws -> Bool
) rethrows -> (ArraySlice<Element>, ReversedCollection<ArraySlice<Element>>) {
guard !self.isEmpty else {
let emptyArraySlice = [Element]()[0...]
return (
emptyArraySlice,
emptyArraySlice.reversed()
)
}
// Since `RandomAccessCollection`s have known sizes (access to `count` is
// constant time, O(1)), we can allocate one array of size `self.count`,
// then insert items at the beginning or end of that contiguous block. This
// way, we don’t have to do any dynamic array resizing. Since we insert the
// right elements on the right side in reverse order, we need to reverse
// them back to the original order at the end.
let count = self.count
// Inside of the `initializer` closure, we set what the actual mid-point is.
// We will use this to partitioned the single array into two in constant time.
var midPoint: Int = 0
let elements = try [Element](
unsafeUninitializedCapacity: count,
initializingWith: { buffer, initializedCount in
var lhs = buffer.baseAddress!
var rhs = lhs + buffer.count
do {
for element in self {
if try belongsInSecondCollection(element) {
rhs -= 1
rhs.initialize(to: element)
} else {
lhs.initialize(to: element)
lhs += 1
}
}
let rhsIndex = rhs - buffer.baseAddress!
initializedCount = buffer.count
midPoint = rhsIndex
} catch {
let lhsCount = lhs - buffer.baseAddress!
let rhsCount = (buffer.baseAddress! + buffer.count) - rhs
buffer.baseAddress!.deinitialize(count: lhsCount)
rhs.deinitialize(count: rhsCount)
throw error
}
})
let lhs = elements[..<midPoint]
let rhs = elements[midPoint...]
return (lhs, rhs.reversed())
}
} benchmark.addSimple(
title: "Array, Array",
input: [Int].self
) { input in
blackHole(input.partitionedA({
$0.isMultiple(of: 2)
}))
}
benchmark.addSimple(
title: "ArraySlice, ArraySlice",
input: [Int].self
) { input in
blackHole(input.partitionedB({
$0.isMultiple(of: 2)
}))
}
benchmark.addSimple(
title: "ArraySlice, ReversedCollection<ArraySlice>",
input: [Int].self
) { input in
blackHole(input.partitionedC({
$0.isMultiple(of: 2)
}))
} I’m a bit surprised that the I wish there were a clear best implementation from a performance point of view. However, since the performance for the non- |
That's interesting and indeed surprising.
I completely agree with your conclusions here. I'd still be interested to see how returning |
I think if I’m following you correctly, that should be the same as the test I ran earlier:
|
I missed that, my bad. Looks like |
…implementation This constant was determined using benchmarking. More information: apple#152 (comment)
…implementation This constant was determined using benchmarking. More information: apple#152 (comment)
`partitioned(_:)` works like `filter(_:)`, but also returns the excluded elements by returning a tuple of two `Array`s
…implementation This constant was determined using benchmarking. More information: apple#152 (comment)
… the `Collection` implementation
Co-authored-by: Xiaodi Wu <13952+xwu@users.noreply.github.com>
Co-authored-by: Xiaodi Wu <13952+xwu@users.noreply.github.com>
The parameter name was potentially confusing. Unlike the other `partition` functions, this function can rely on its named tuple to clarify its behavior.
@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 good! Mostly documentation nits, and then I think this is ready to merge 👍🏻
Co-authored-by: Nate Cook <natecook@apple.com>
Co-authored-by: Nate Cook <natecook@apple.com>
…r of actual elements found while iterating
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.
@swift-ci Please test |
Thank you @timvermeulen, @natecook1000, @xwu, @LucianoPAlmeida, @fedeci, and @CTMacUser for helping me get this function integrated into swift-algorithms! |
Description
Adds a
partitioned(by:)
algorithm. This is very similar tofilter(_:)
, but instead of just getting an array of the elements that do match a given predicate, also get a second array for the elements that did not match the same predicate.This is more performant than calling
filter(_:)
twice on the same input with mutually-exclusive predicates since:Detailed Design
Naming
At a high-level, this acts similarly to the
partition
family of functions in that it separates all the elements in a given collection in two parts, those that do and do not match a given predicate. Thanks, @timvermeulen for help with naming!Documentation Plan
Test Plan
Source Impact
This is purely additive
Checklist