Skip to content

Commit 846a861

Browse files
authored
Improve RangeSet initialization performance (#75089)
When initializing a range set with a group of overlapping, identical, or empty ranges, the initializer can exhibit poor performance due to removing the unneeded ranges during processing. This change uses a partitioning scheme instead, only removing the unnecessary ranges at the end of initialization.
1 parent 8bb6b30 commit 846a861

File tree

2 files changed

+89
-16
lines changed

2 files changed

+89
-16
lines changed

stdlib/public/core/RangeSetRanges.swift

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,25 +37,57 @@ extension RangeSet {
3737
_storage.sort {
3838
$0.lowerBound < $1.lowerBound
3939
}
40-
var i = 0
41-
while i < _storage.count {
42-
let current = _storage[i]
43-
if i > 0 {
44-
let previous = _storage[i - 1]
45-
if previous.upperBound >= current.lowerBound {
46-
let newUpper = Swift.max(previous.upperBound, current.upperBound)
47-
_storage[i - 1] = previous.lowerBound ..< newUpper
48-
_storage.remove(at: i)
49-
continue
50-
}
51-
}
52-
53-
if current.isEmpty {
54-
_storage.remove(at: i)
40+
41+
// Find the index of the first non-empty range. If all ranges are empty,
42+
// the result is empty.
43+
guard let firstNonEmpty = _storage.firstIndex(where: { $0.isEmpty == false }) else {
44+
_storage = []
45+
return
46+
}
47+
48+
// Swap that non-empty range to be first. (This and the swap in the loop
49+
// might be no-ops, if no empty or overlapping ranges have been found.)
50+
_storage.swapAt(0, firstNonEmpty)
51+
52+
// That single range is now a valid range set, so we set up three sections
53+
// of the storage array:
54+
//
55+
// 1: a processed, valid range set (0...lastValid)
56+
// 2: ranges to discard (lastValid + 1 ..< current)
57+
// 3: unprocessed ranges (current ..< _storage.count)
58+
//
59+
// Section 2 is made up of ranges that are either empty or that overlap
60+
// with the ranges in section 1. By waiting to remove these ranges until
61+
// we've processed the entire array, we avoid needing to constantly
62+
// reshuffle the elements during processing.
63+
var lastValid = 0
64+
var current = firstNonEmpty + 1
65+
66+
while current < _storage.count {
67+
defer { current += 1 }
68+
69+
// Skip over empty ranges.
70+
if _storage[current].isEmpty { continue }
71+
72+
// If the last valid range overlaps with the current range, extend the
73+
// last valid range to cover the current.
74+
if _storage[lastValid].upperBound >= _storage[current].lowerBound {
75+
let newUpper = Swift.max(
76+
_storage[lastValid].upperBound,
77+
_storage[current].upperBound)
78+
_storage[lastValid] = Range(
79+
uncheckedBounds: (_storage[lastValid].lowerBound, newUpper))
5580
} else {
56-
i += 1
81+
// Otherwise, this is a valid new range to add to the range set:
82+
// swap it into place at the end of the valid section.
83+
lastValid += 1
84+
_storage.swapAt(current, lastValid)
5785
}
5886
}
87+
88+
// Now that we've processed the whole array, remove anything left after
89+
// the valid section.
90+
_storage.removeSubrange((lastValid + 1) ..< _storage.count)
5991
}
6092
}
6193
}

validation-test/stdlib/RangeSet.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,47 @@ if #available(SwiftStdlib 6.0, *) {
4242
}
4343
return set
4444
}
45+
46+
RangeSetTests.test("initialization") {
47+
// Test coalescing and elimination of empty ranges
48+
do {
49+
let empty = RangeSet(Array(repeating: 0..<0, count: 100))
50+
expectTrue(empty.isEmpty)
51+
52+
let repeated = RangeSet(Array(repeating: 0..<3, count: 100))
53+
expectEqual(repeated, [0..<3])
54+
55+
let singleAfterEmpty = RangeSet(Array(repeating: 0..<0, count: 100) + [0..<3])
56+
expectEqual(singleAfterEmpty, [0..<3])
57+
58+
let contiguousRanges = (0..<100).map { $0 ..< ($0 + 1) }
59+
expectEqual(RangeSet(contiguousRanges), [0..<100])
60+
expectEqual(RangeSet(contiguousRanges.shuffled()), [0..<100])
61+
}
62+
63+
// The `buildRandomRangeSet()` function builds a range set via additions
64+
// and removals. This function creates an array of potentially empty or
65+
// overlapping ranges that can be used to initialize a range set.
66+
func randomRanges() -> [Range<Int>] {
67+
(0..<100).map { _ in
68+
let low = Int.random(in: 0...100)
69+
let count = Int.random(in: 0...20)
70+
return low ..< (low + count)
71+
}
72+
}
73+
74+
for _ in 0..<1000 {
75+
let ranges = randomRanges()
76+
let set = RangeSet(ranges)
77+
78+
// Manually construct a range set for comparison
79+
var comparison = RangeSet<Int>()
80+
for r in ranges {
81+
comparison.insert(contentsOf: r)
82+
}
83+
expectEqual(set, comparison)
84+
}
85+
}
4586

4687
RangeSetTests.test("contains") {
4788
expectFalse(source.contains(0))

0 commit comments

Comments
 (0)