Skip to content

Commit

Permalink
Merge pull request #235 from lorentey/PersistentCollections-review-prep
Browse files Browse the repository at this point in the history
[PersistentCollections] Doc & benchmark updates in preparation of API review
  • Loading branch information
lorentey committed Nov 1, 2022
2 parents 0e230a9 + ff2d526 commit ff56114
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 31 deletions.
8 changes: 7 additions & 1 deletion Benchmarks/Libraries/PersistentSet.json
Expand Up @@ -278,9 +278,15 @@
"title": "insert",
"tasks": [
"PersistentSet<Int> insert",
"PersistentSet<Int> insert, shared",
"Set<Int> insert",
"Set<Int> insert, reserving capacity",
]
},
{
"kind": "chart",
"title": "insert-shared",
"tasks": [
"PersistentSet<Int> insert, shared",
"Set<Int> insert, shared",
]
},
Expand Down
31 changes: 20 additions & 11 deletions Benchmarks/Sources/Benchmarks/PersistentSetBenchmarks.swift
Expand Up @@ -120,18 +120,27 @@ extension Benchmark {
}
}

do {
var timer = Timer()
let input = 0 ..< 1_000
let newMember = input.count
self.addSimple(
title: "PersistentSet<Int> model diffing",
input: Int.self
) { input in
typealias Model = PersistentSet<Int>

let original = PersistentSet(input)
timer.measure {
var copy = original
copy.insert(newMember)
let diff = copy.subtracting(original)
precondition(diff.count == 1 && diff.first == newMember)
blackHole(copy)
var _state: Model = [] // Private
func updateState(
with model: Model
) -> (insertions: Model, removals: Model) {
let insertions = model.subtracting(_state)
let removals = _state.subtracting(model)
_state = model
return (insertions, removals)
}

var model: Model = []
for i in 0 ..< input {
model.insert(i)
let r = updateState(with: model)
precondition(r.insertions.count == 1 && r.removals.count == 0)
}
}

Expand Down
24 changes: 24 additions & 0 deletions Benchmarks/Sources/Benchmarks/SetBenchmarks.swift
Expand Up @@ -152,6 +152,30 @@ extension Benchmark {
}
}

self.addSimple(
title: "Set<Int> model diffing",
input: Int.self
) { input in
typealias Model = Set<Int>

var _state: Model = [] // Private
func updateState(
with model: Model
) -> (insertions: Model, removals: Model) {
let insertions = model.subtracting(_state)
let removals = _state.subtracting(model)
_state = model
return (insertions, removals)
}

var model: Model = []
for i in 0 ..< input {
model.insert(i)
let r = updateState(with: model)
precondition(r.insertions.count == 1 && r.removals.count == 0)
}
}

self.add(
title: "Set<Int> remove",
input: ([Int], [Int]).self
Expand Down
Expand Up @@ -11,6 +11,15 @@

### Collection Views

`PersistentDictionary` provides the customary dictionary views, `keys` and
`values`. These are collection types that are projections of the dictionary
itself, with elements that match only the keys or values of the dictionary,
respectively. The `Keys` view is notable in that it provides operations for
subtracting and intersecting the keys of two dictionaries, allowing for easy
detection of inserted and removed items between two snapshots of the same
dictionary. Because `PersistentDictionary` needs to invalidate indices on every
mutation, its `Values` view is not a `MutableCollection`.

- ``Keys-swift.struct``
- ``Values-swift.struct``
- ``keys-swift.property``
Expand Down Expand Up @@ -43,6 +52,13 @@

### Adding or Updating Keys and Values

Beyond the standard `updateValue(_:forKey:)` method, `PersistentDictionary` also
provides additional `updateValue` variants that take closure arguments. These
provide a more straightforward way to perform in-place mutations on dictionary
values (compared to mutating values through the corresponding subscript
operation.) `PersistentDictionary` also provides the standard `merge` and
`merging` operations for combining dictionary values.

- ``updateValue(_:forKey:)``
- ``updateValue(forKey:with:)``
- ``updateValue(forKey:default:with:)``
Expand All @@ -57,6 +73,7 @@

- ``removeValue(forKey:)``
- ``remove(at:)``
- ``filter(_:)``

### Non-mutating Dictionary Operations

Expand All @@ -69,7 +86,6 @@

### Transforming a Dictionary

- ``filter(_:)``
- ``mapValues(_:)``
- ``compactMapValues(_:)``

25 changes: 21 additions & 4 deletions Sources/Collections/Collections.docc/Extensions/PersistentSet.md
Expand Up @@ -19,7 +19,7 @@ data structure based on their hash values. For example, assuming 16 bit hash
values sliced into 4-bit chunks, each node in the prefix tree would have
sixteen slots (one for each digit), each of which may contain a member, a
child node reference, or it may be empty. A `PersistentSet` containing the
tree items `Maximo`, `Julia` and `Don Pablo` (with hash values of `0x2B65`,
three items `Maximo`, `Julia` and `Don Pablo` (with hash values of `0x2B65`,
`0xA69F` and `0xADA1`, respectively) may be organized into a prefix tree of
two nodes:

Expand Down Expand Up @@ -69,8 +69,6 @@ As long as `Element` properly implements `Hashable`, lookup operations in a
particular item by looking at no more than a constant number of items on
average -- typically they will need to compare against just one member.

<!-- ## Overview -->

## Topics

### Creating a Set
Expand All @@ -94,18 +92,30 @@ average -- typically they will need to compare against just one member.

### Removing Elements

- ``filter(_:)``
- ``remove(_:)``
- ``remove(at:)``
- ``filter(_:)``

### Non-mutating Set Operations

To acknowledge the efficiency of `PersistentSet` at inserting and removing items
from copies of a set, we provide a family of non-mutating primitives that
perform these operations. Instead of directly modifying the set they're given,
they return a brand new set with the desired result.

- ``inserting(_:)``
- ``updating(with:)``
- ``removing(_:)``

### Combining Sets

All the standard combining operations (intersection, union, subtraction and
symmetric difference) are supported, in both non-mutating and mutating forms.
`SetAlgebra` only requires the ability to combine one set instance with another,
but `PersistentSet` follows the tradition established by `Set` in providing
additional overloads to each operation that allow combining a set with
additional types, including arbitrary sequences.

- ``intersection(_:)-9dx2a``
- ``intersection(_:)-5z4my``
- ``intersection(_:)-1d4e0``
Expand Down Expand Up @@ -140,6 +150,13 @@ average -- typically they will need to compare against just one member.

### Comparing Sets

`PersistentSet` supports all standard set comparisons (subset tests, superset
tests, disjunctness test), including the customary overloads established by
`Set`. As an additional extension, the `isEqualSet` family of member functions
generalize the standard `==` operation to support checking whether a
`PersistentSet` consists of exactly the same members as an arbitrary sequence,
ignoring element ordering and duplicates (if any).

- ``==(_:_:)``
- ``isEqualSet(to:)-7egih``
- ``isEqualSet(to:)-4t0k5``
Expand Down
Expand Up @@ -8,6 +8,15 @@

### Collection Views

`PersistentDictionary` provides the customary dictionary views, `keys` and
`values`. These are collection types that are projections of the dictionary
itself, with elements that match only the keys or values of the dictionary,
respectively. The `Keys` view is notable in that it provides operations for
subtracting and intersecting the keys of two dictionaries, allowing for easy
detection of inserted and removed items between two snapshots of the same
dictionary. Because `PersistentDictionary` needs to invalidate indices on every
mutation, its `Values` view is not a `MutableCollection`.

- ``Keys-swift.struct``
- ``Values-swift.struct``
- ``keys-swift.property``
Expand Down Expand Up @@ -40,6 +49,13 @@

### Adding or Updating Keys and Values

Beyond the standard `updateValue(_:forKey:)` method, `PersistentDictionary` also
provides additional `updateValue` variants that take closure arguments. These
provide a more straightforward way to perform in-place mutations on dictionary
values (compared to mutating values through the corresponding subscript
operation.) `PersistentDictionary` also provides the standard `merge` and
`merging` operations for combining dictionary values.

- ``updateValue(_:forKey:)``
- ``updateValue(forKey:with:)``
- ``updateValue(forKey:default:with:)``
Expand All @@ -54,6 +70,7 @@

- ``removeValue(forKey:)``
- ``remove(at:)``
- ``filter(_:)``

### Non-mutating Dictionary Operations

Expand All @@ -66,7 +83,6 @@

### Transforming a Dictionary

- ``filter(_:)``
- ``mapValues(_:)``
- ``compactMapValues(_:)``

Expand Up @@ -16,7 +16,7 @@ data structure based on their hash values. For example, assuming 16 bit hash
values sliced into 4-bit chunks, each node in the prefix tree would have
sixteen slots (one for each digit), each of which may contain a member, a
child node reference, or it may be empty. A `PersistentSet` containing the
tree items `Maximo`, `Julia` and `Don Pablo` (with hash values of `0x2B65`,
three items `Maximo`, `Julia` and `Don Pablo` (with hash values of `0x2B65`,
`0xA69F` and `0xADA1`, respectively) may be organized into a prefix tree of
two nodes:

Expand Down Expand Up @@ -66,8 +66,6 @@ As long as `Element` properly implements `Hashable`, lookup operations in a
particular item by looking at no more than a constant number of items on
average -- typically they will need to compare against just one member.

<!-- ## Overview -->

## Topics

### Creating a Set
Expand All @@ -91,18 +89,30 @@ average -- typically they will need to compare against just one member.

### Removing Elements

- ``filter(_:)``
- ``remove(_:)``
- ``remove(at:)``
- ``filter(_:)``

### Non-mutating Set Operations

To acknowledge the efficiency of `PersistentSet` at inserting and removing items
from copies of a set, we provide a family of non-mutating primitives that
perform these operations. Instead of directly modifying the set they're given,
they return a brand new set with the desired result.

- ``inserting(_:)``
- ``updating(with:)``
- ``removing(_:)``

### Combining Sets

All the standard combining operations (intersection, union, subtraction and
symmetric difference) are supported, in both non-mutating and mutating forms.
`SetAlgebra` only requires the ability to combine one set instance with another,
but `PersistentSet` follows the tradition established by `Set` in providing
additional overloads to each operation that allow combining a set with
additional types, including arbitrary sequences.

- ``intersection(_:)-9dx2a``
- ``intersection(_:)-5z4my``
- ``intersection(_:)-1d4e0``
Expand Down Expand Up @@ -137,6 +147,13 @@ average -- typically they will need to compare against just one member.

### Comparing Sets

`PersistentSet` supports all standard set comparisons (subset tests, superset
tests, disjunctness test), including the customary overloads established by
`Set`. As an additional extension, the `isEqualSet` family of member functions
generalize the standard `==` operation to support checking whether a
`PersistentSet` consists of exactly the same members as an arbitrary sequence,
ignoring element ordering and duplicates (if any).

- ``==(_:_:)``
- ``isEqualSet(to:)-7egih``
- ``isEqualSet(to:)-4t0k5``
Expand Down
Expand Up @@ -20,18 +20,18 @@
/// neither type provides any guarantees about the ordering of their items.
///
/// However, `PersistentDictionary` is optimizing specifically for use cases
/// that need to mutate shared copies or to compare a set value to one of its
/// older snapshots.
/// that need to mutate shared copies or to compare a dictionary value to one of
/// its older snapshots.
///
/// The standard `Dictionary` stores its members in a single, flat hash table,
/// and it implements value semantics with all-or-nothing copy-on-write
/// behavior: every time a shared copy of a set is mutated, the mutation needs
/// to make a full copy of the set's storage. `PersistentDictionary` takes a
/// different approach: it organizes its members into a tree structure, the
/// nodes of which can be freely shared across collection values. When mutating
/// a shared copy of a set value, `PersistentSet` is able to simply link the
/// unchanged parts of the tree directly into the result, saving both time and
/// memory.
/// behavior: every time a shared copy of a dictionary is mutated, the mutation
/// needs to make a full copy of the dictionary's storage.
/// `PersistentDictionary` takes a different approach: it organizes its members
/// into a tree structure, the nodes of which can be freely shared across
/// collection values. When mutating a shared copy of a dictionary value,
/// `PersistentDictionary` is able to simply link the unchanged parts of the
/// tree directly into the result, saving both time and memory.
///
/// This structural sharing also makes it more efficient to compare mutated
/// dictionaries values to earlier versions of themselves. When comparing or
Expand Down

0 comments on commit ff56114

Please sign in to comment.