Skip to content

Commit 62adcfa

Browse files
committed
Add exhaustive SortedSet tests and benchmarks.
Key Changes: - Add tests and benchmarks for SortedSet - Fix bug during _Node.UnsafeHandle.popFirstElement() rebalancing - Enable _Node.UnsafeHandle.subscript(elementAt:) to work on value-less nodes. Minor Changes: - Optimize Codable to use _BTree.Builder and forEach - Codable now checks for order, not strictly uniqueness - Add smaller node size for debug builds to easily catch errors
1 parent cf67226 commit 62adcfa

File tree

13 files changed

+798
-54
lines changed

13 files changed

+798
-54
lines changed

Benchmarks/Benchmarks/SortedDictionaryBenchmarks.swift

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ extension Benchmark {
1818
title: "SortedDictionary<Int, Int> init(keysWithValues:)",
1919
input: [Int].self
2020
) { input in
21-
let keysAndValues = input.lazy.map { (key: $0, value: 2 * $0) }
21+
let keysAndValues = input.map { (key: $0, value: 2 * $0) }
2222

2323
return { timer in
2424
blackHole(SortedDictionary(keysWithValues: keysAndValues))
@@ -35,6 +35,20 @@ extension Benchmark {
3535
blackHole(SortedDictionary(sortedKeysWithValues: keysAndValues))
3636
}
3737
}
38+
39+
self.add(
40+
title: "SortedDictionary<Int, Int> sort, then init(sortedKeysWithValues:)",
41+
input: [Int].self
42+
) { input in
43+
return { timer in
44+
var keysAndValues = input.map { (key: $0, value: 2 * $0) }
45+
46+
timer.measure {
47+
keysAndValues.sort(by: { $0.key < $1.key })
48+
blackHole(SortedDictionary(sortedKeysWithValues: keysAndValues))
49+
}
50+
}
51+
}
3852

3953
self.add(
4054
title: "SortedDictionary<Int, Int> sequential iteration",
@@ -50,6 +64,38 @@ extension Benchmark {
5064
}
5165
}
5266

67+
self.add(
68+
title: "SortedDictionary<Int, Int> index-based iteration",
69+
input: [Int].self
70+
) { input in
71+
let keysAndValues = input.lazy.map { (key: $0, value: 2 * $0) }
72+
let d = SortedDictionary(keysWithValues: keysAndValues)
73+
74+
return { timer in
75+
var i = d.startIndex
76+
while i != d.endIndex {
77+
blackHole(d[i])
78+
d.formIndex(after: &i)
79+
}
80+
}
81+
}
82+
83+
self.add(
84+
title: "SortedDictionary<Int, Int> offset-based iteration",
85+
input: [Int].self
86+
) { input in
87+
let keysAndValues = input.lazy.map { (key: $0, value: 2 * $0) }
88+
let d = SortedDictionary(keysWithValues: keysAndValues)
89+
90+
return { timer in
91+
for offset in 0..<keysAndValues.count {
92+
var i = d.endIndex
93+
d.formIndex(&i, offsetBy: offset - d.count)
94+
blackHole(d[i])
95+
}
96+
}
97+
}
98+
5399
self.add(
54100
title: "SortedDictionary<Int, Int> forEach iteration",
55101
input: [Int].self
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Collections open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
import CollectionsBenchmark
13+
import SortedCollections
14+
15+
extension Benchmark {
16+
public mutating func addSortedSetBenchmarks() {
17+
self.addSimple(
18+
title: "SortedSet<Int> init from range",
19+
input: Int.self
20+
) { size in
21+
blackHole(SortedSet(0 ..< size))
22+
}
23+
24+
self.addSimple(
25+
title: "SortedSet<Int> init(sortedElements:) from range",
26+
input: Int.self
27+
) { size in
28+
blackHole(SortedSet(sortedElements: 0 ..< size))
29+
}
30+
self.add(
31+
title: "SortedSet<Int> sequential iteration",
32+
input: [Int].self
33+
) { input in
34+
let set = SortedSet(input)
35+
return { timer in
36+
for i in set {
37+
blackHole(i)
38+
}
39+
}
40+
}
41+
42+
self.add(
43+
title: "SortedSet<Int> forEach iteration",
44+
input: [Int].self
45+
) { input in
46+
let set = SortedSet(input)
47+
return { timer in
48+
set.forEach { i in
49+
blackHole(i)
50+
}
51+
}
52+
}
53+
54+
self.add(
55+
title: "SortedSet<Int> successful contains",
56+
input: ([Int], [Int]).self
57+
) { input, lookups in
58+
let set = SortedSet(input)
59+
return { timer in
60+
for i in lookups {
61+
precondition(set.contains(i))
62+
}
63+
}
64+
}
65+
66+
self.add(
67+
title: "SortedSet<Int> unsuccessful contains",
68+
input: ([Int], [Int]).self
69+
) { input, lookups in
70+
let set = SortedSet(input)
71+
let lookups = lookups.map { $0 + input.count }
72+
return { timer in
73+
for i in lookups {
74+
precondition(!set.contains(i))
75+
}
76+
}
77+
}
78+
79+
self.addSimple(
80+
title: "SortedSet<Int> insertions",
81+
input: [Int].self
82+
) { input in
83+
var set: SortedSet<Int> = []
84+
for i in input {
85+
set.insert(i)
86+
}
87+
precondition(set.count == input.count)
88+
blackHole(set)
89+
}
90+
91+
self.add(
92+
title: "SortedSet<Int> remove",
93+
input: ([Int], [Int]).self
94+
) { input, removals in
95+
return { timer in
96+
var set = SortedSet(input)
97+
timer.measure {
98+
for i in removals {
99+
set.remove(i)
100+
}
101+
}
102+
precondition(set.isEmpty)
103+
blackHole(set)
104+
}
105+
}
106+
107+
self.add(
108+
title: "SortedSet<Int> removeLast",
109+
input: Int.self
110+
) { size in
111+
return { timer in
112+
var set = SortedSet(0 ..< size)
113+
timer.measure {
114+
for _ in 0 ..< size {
115+
set.removeLast()
116+
}
117+
}
118+
precondition(set.isEmpty)
119+
blackHole(set)
120+
}
121+
}
122+
123+
self.add(
124+
title: "SortedSet<Int> removeFirst",
125+
input: Int.self
126+
) { size in
127+
return { timer in
128+
var set = SortedSet(0 ..< size)
129+
timer.measure {
130+
for _ in 0 ..< size {
131+
set.removeFirst()
132+
}
133+
}
134+
precondition(set.isEmpty)
135+
blackHole(set)
136+
}
137+
}
138+
139+
let overlaps: [(String, (Int) -> Int)] = [
140+
("0%", { c in c }),
141+
("25%", { c in 3 * c / 4 }),
142+
("50%", { c in c / 2 }),
143+
("75%", { c in c / 4 }),
144+
("100%", { c in 0 }),
145+
]
146+
147+
// SetAlgebra operations with Self
148+
do {
149+
for (percentage, start) in overlaps {
150+
self.add(
151+
title: "SortedSet<Int> union with Self (\(percentage) overlap)",
152+
input: [Int].self
153+
) { input in
154+
let start = start(input.count)
155+
let a = SortedSet(input)
156+
let b = SortedSet(start ..< start + input.count)
157+
return { timer in
158+
blackHole(a.union(b))
159+
}
160+
}
161+
}
162+
163+
for (percentage, start) in overlaps {
164+
self.add(
165+
title: "SortedSet<Int> intersection with Self (\(percentage) overlap)",
166+
input: [Int].self
167+
) { input in
168+
let start = start(input.count)
169+
let a = SortedSet(input)
170+
let b = SortedSet(start ..< start + input.count)
171+
return { timer in
172+
blackHole(a.intersection(b))
173+
}
174+
}
175+
}
176+
177+
for (percentage, start) in overlaps {
178+
self.add(
179+
title: "SortedSet<Int> symmetricDifference with Self (\(percentage) overlap)",
180+
input: [Int].self
181+
) { input in
182+
let start = start(input.count)
183+
let a = SortedSet(input)
184+
let b = SortedSet(start ..< start + input.count)
185+
return { timer in
186+
blackHole(a.symmetricDifference(b))
187+
}
188+
}
189+
}
190+
191+
for (percentage, start) in overlaps {
192+
self.add(
193+
title: "SortedSet<Int> subtracting Self (\(percentage) overlap)",
194+
input: [Int].self
195+
) { input in
196+
let start = start(input.count)
197+
let a = SortedSet(input)
198+
let b = SortedSet(start ..< start + input.count)
199+
return { timer in
200+
blackHole(a.subtracting(b))
201+
}
202+
}
203+
}
204+
}
205+
206+
}
207+
}

Benchmarks/swift-collections-benchmark/main.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ benchmark.addDictionaryBenchmarks()
1919
benchmark.addDequeBenchmarks()
2020
benchmark.addOrderedSetBenchmarks()
2121
benchmark.addOrderedDictionaryBenchmarks()
22+
benchmark.addSortedSetBenchmarks()
2223
benchmark.addSortedDictionaryBenchmarks()
2324
benchmark.addHeapBenchmarks()
2425
benchmark.addCppBenchmarks()

Sources/SortedCollections/BTree/_BTree.swift

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,28 +19,28 @@
1919
@usableFromInline
2020
internal struct _BTree<Key: Comparable, Value> {
2121

22-
/// Internal node size in bytes for B-Tree
23-
@inlinable
24-
@inline(__always)
25-
internal static var defaultInternalSize: Int { 128 }
26-
2722
/// Recommended node size of a given B-Tree
2823
@inlinable
2924
@inline(__always)
3025
internal static var defaultInternalCapacity: Int {
31-
Swift.min(16, _BTree.defaultInternalSize / MemoryLayout<Key>.stride)
26+
#if DEBUG
27+
return 4
28+
#else
29+
let capacityInBytes = 128
30+
return Swift.min(16, capacityInBytes / MemoryLayout<Key>.stride)
31+
#endif
3232
}
3333

34-
/// Leaf node capacity for B-Tree
35-
@inlinable
36-
@inline(__always)
37-
internal static var defaultLeafSize: Int { 2000 }
38-
3934
/// Recommended node size of a given B-Tree
4035
@inlinable
4136
@inline(__always)
4237
internal static var defaultLeafCapacity: Int {
43-
Swift.min(16, _BTree.defaultLeafSize / MemoryLayout<Key>.stride)
38+
#if DEBUG
39+
return 5
40+
#else
41+
let capacityInBytes = 2000
42+
return Swift.min(16, capacityInBytes / MemoryLayout<Key>.stride)
43+
#endif
4444
}
4545

4646
/// The element type of the collection.

Sources/SortedCollections/BTree/_Node.UnsafeHandle+Deletion.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ extension _Node.UnsafeHandle {
155155

156156
self.subtreeCount -= 1
157157

158-
self.balance(atSlot: self.childCount - 1)
158+
self.balance(atSlot: 0)
159159
return poppedElement
160160
}
161161

Sources/SortedCollections/BTree/_Node.UnsafeHandle.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,11 @@ extension _Node.UnsafeHandle {
235235
get {
236236
assert(0 <= slot && slot < self.elementCount,
237237
"Node element subscript out of bounds.")
238-
return (key: self[keyAt: slot], value: self[valueAt: slot])
238+
if _Node.hasValues {
239+
return (key: self[keyAt: slot], value: self[valueAt: slot])
240+
} else {
241+
return (key: self[keyAt: slot], value: _Node.dummyValue)
242+
}
239243
}
240244
}
241245

Sources/SortedCollections/SortedDictionary/SortedDictionary+Codable.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ extension SortedDictionary: Encodable where Key: Codable, Value: Codable {
3030
public func encode(to encoder: Encoder) throws {
3131
// Encode contents as an array of alternating key-value pairs.
3232
var container = encoder.unkeyedContainer()
33-
for (key, value) in self {
33+
try self.forEach { (key, value) in
3434
try container.encode(key)
3535
try container.encode(value)
3636
}
@@ -42,8 +42,9 @@ extension SortedDictionary: Decodable where Key: Decodable, Value: Decodable {
4242
public init(from decoder: Decoder) throws {
4343
// We expect to be encoded as an array of alternating key-value pairs.
4444
var container = try decoder.unkeyedContainer()
45+
var builder = _Tree.Builder(deduplicating: true)
46+
var previousKey: Key? = nil
4547

46-
self.init()
4748
while !container.isAtEnd {
4849
let key = try container.decode(Key.self)
4950

@@ -57,13 +58,18 @@ extension SortedDictionary: Decodable where Key: Decodable, Value: Decodable {
5758
}
5859

5960
let value = try container.decode(Value.self)
60-
let oldValue = self.updateValue(value, forKey: key)
61-
if oldValue != nil {
61+
62+
guard previousKey == nil || previousKey! < key else {
6263
let context = DecodingError.Context(
6364
codingPath: container.codingPath,
64-
debugDescription: "Duplicate key at offset \(container.currentIndex - 1)")
65+
debugDescription: "Decoded elements out of order.")
6566
throw DecodingError.dataCorrupted(context)
6667
}
68+
69+
builder.append((key, value))
70+
previousKey = key
6771
}
72+
73+
self.init(_rootedAt: builder.finish())
6874
}
6975
}

0 commit comments

Comments
 (0)