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)
timvermeulen
left a comment
There was a problem hiding this comment.
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 |
natecook1000
left a comment
There was a problem hiding this comment.
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
|
@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
partitionfamily 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