Skip to content
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

Persistent collections updates (part 5) #179

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2e523b1
[PersistentCollections] Reverse ordering of items in node storage
lorentey Sep 16, 2022
3e38184
[PersistentCollections] Optimize _Node.==
lorentey Sep 16, 2022
edaadc9
[PersistentCollections] _Level: Store the shift amount in an UInt8
lorentey Sep 16, 2022
6dd0211
[PersistentCollections] Flesh out _RawNode, add _UnmanagedNode
lorentey Sep 16, 2022
547ac90
[PersistentCollections] Rework basic node properties
lorentey Sep 16, 2022
02133f1
[PersistentCollections] Implement path-based indices
lorentey Sep 16, 2022
16393ea
[PersistentCollections] Allow dumping hash trees in iteration order
lorentey Sep 16, 2022
bf3617e
[PersistentCollections] Work on testing a bit; add fixtures
lorentey Sep 16, 2022
0fe7f08
[PersistentCollections] offset → slot
lorentey Sep 16, 2022
52e3ea6
[test] LifetimeTracked: Implement high-fidelity hash forwarding
lorentey Sep 16, 2022
6cf128e
[PersistentCollections] Internal doc updates
lorentey Sep 16, 2022
1324c4e
[OrderedDictionary] Implement index invalidation
lorentey Sep 16, 2022
1661bd4
[PersistentDictionary] Implement in-place mutations for defaulted sub…
lorentey Sep 16, 2022
3b7d241
[PersistentDictionary] Add some docs
lorentey Sep 16, 2022
57deed5
[PersistentDictionary] Implement in-place mutations
lorentey Sep 17, 2022
2321b34
[PersistentCollections] Fix node sizing logic
lorentey Sep 17, 2022
5240bf6
[PersistentCollections] Reduce _Bucket’s storage size
lorentey Sep 18, 2022
d45f264
[PersistentDictionary] Fix index(forKey:) performance
lorentey Sep 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -19,7 +19,7 @@ extension _Node: CustomStringConvertible {
var result = "["
var first = true
read {
for (key, value) in $0._items {
for (key, value) in $0.reverseItems.reversed() {
if first {
first = false
} else {
Expand Down
19 changes: 9 additions & 10 deletions Sources/PersistentCollections/Node/_Node+Debugging.swift
Expand Up @@ -34,6 +34,12 @@ extension _Node.Storage {
}

extension _Node.UnsafeHandle {
internal func _itemString(at offset: Int) -> String {
let item = self[item: offset]
let hash = _Hash(item.key).description
return "hash: \(hash), key: \(item.key), value: \(item.value)"
}

@usableFromInline
internal func dump(
firstPrefix: String = "",
Expand All @@ -50,12 +56,8 @@ extension _Node.UnsafeHandle {
""")
guard limit > 0 else { return }
if isCollisionNode {
let items = self._items
for offset in items.indices {
let item = items[offset]
let hash = _Hash(item.key).description
let itemStr = "hash: \(hash), key: \(item.key), value: \(item.value)"
print("\(restPrefix) \(offset): \(itemStr)")
for offset in 0 ..< itemCount {
print("\(restPrefix)[\(offset)] \(_itemString(at: offset))")
}
} else {
var itemOffset = 0
Expand All @@ -64,10 +66,7 @@ extension _Node.UnsafeHandle {
let bucket = _Bucket(b)
let bucketStr = "#\(String(b, radix: _Bitmap.capacity, uppercase: true))"
if itemMap.contains(bucket) {
let item = self[item: itemOffset]
let hash = _Hash(item.key).description
let itemStr = "hash: \(hash), key: \(item.key), value: \(item.value)"
print("\(restPrefix) \(bucketStr) \(itemStr)")
print("\(restPrefix) \(bucketStr) \(_itemString(at: itemOffset))")
itemOffset += 1
} else if childMap.contains(bucket) {
self[child: childOffset].dump(
Expand Down
6 changes: 3 additions & 3 deletions Sources/PersistentCollections/Node/_Node+Equatable.swift
Expand Up @@ -18,9 +18,9 @@ extension _Node: Equatable where Value: Equatable {
return lhs.read { lhs in
rhs.read { rhs in
let l = Dictionary(
uniqueKeysWithValues: lhs._items.lazy.map { ($0.key, $0.value) })
uniqueKeysWithValues: lhs.reverseItems.lazy.map { ($0.key, $0.value) })
let r = Dictionary(
uniqueKeysWithValues: rhs._items.lazy.map { ($0.key, $0.value) })
uniqueKeysWithValues: rhs.reverseItems.lazy.map { ($0.key, $0.value) })
return l == r
}
}
Expand All @@ -39,7 +39,7 @@ extension _Node: Equatable where Value: Equatable {
guard l.itemMap == r.itemMap else { return false }
guard l.childMap == r.childMap else { return false }

guard l._items.elementsEqual(r._items, by: { $0 == $1 })
guard l.reverseItems.elementsEqual(r.reverseItems, by: { $0 == $1 })
else { return false }

guard l._children.elementsEqual(r._children) else { return false }
Expand Down
12 changes: 6 additions & 6 deletions Sources/PersistentCollections/Node/_Node+Initializers.swift
Expand Up @@ -20,9 +20,9 @@ extension _Node {
let byteCount = 2 * MemoryLayout<Element>.stride
assert($0.bytesFree >= byteCount)
$0.bytesFree &-= byteCount
let items = $0._items
items.initializeElement(at: 0, to: item1)
items.initializeElement(at: 1, to: item2)
let items = $0.reverseItems
items.initializeElement(at: 1, to: item1)
items.initializeElement(at: 0, to: item2)
}
}

Expand All @@ -39,10 +39,10 @@ extension _Node {
$0.itemMap.insert(bucket1)
$0.itemMap.insert(bucket2)
$0.bytesFree &-= 2 * MemoryLayout<Element>.stride
let i = bucket1 < bucket2 ? 0 : 1
let items = $0._items
let i = bucket1 < bucket2 ? 1 : 0
let items = $0.reverseItems
items.initializeElement(at: i, to: item1)
items.initializeElement(at: 1 - i, to: item2)
items.initializeElement(at: 1 &- i, to: item2)
}
}

Expand Down
7 changes: 4 additions & 3 deletions Sources/PersistentCollections/Node/_Node+Lookups.swift
Expand Up @@ -94,10 +94,11 @@ extension _Node.UnsafeHandle {
return .expansion(h)
}
}
guard let offset = _items.firstIndex(where: { $0.key == key }) else {
return .notFound(.invalid, 0)
// Note: this searches the items in reverse insertion order.
guard let offset = reverseItems.firstIndex(where: { $0.key == key }) else {
return .notFound(.invalid, itemCount)
}
return .found(.invalid, offset)
return .found(.invalid, itemCount &- 1 &- offset)
}
let bucket = hash[level]
if itemMap.contains(bucket) {
Expand Down
36 changes: 19 additions & 17 deletions Sources/PersistentCollections/Node/_Node+Mutations.swift
Expand Up @@ -20,19 +20,20 @@ extension _Node.UnsafeHandle {
@inlinable
internal func _insertItem(_ item: __owned Element, at offset: Int) {
assertMutable()
let count = itemCount
assert(offset >= 0 && offset <= count)
let c = itemCount
assert(offset >= 0 && offset <= c)

let stride = MemoryLayout<Element>.stride
assert(bytesFree >= stride)
bytesFree &-= stride

let start = _memory
.advanced(by: byteCapacity &- (count &+ 1) &* stride)
.bindMemory(to: Element.self, capacity: count &+ 1)
.advanced(by: byteCapacity &- (c &+ 1) &* stride)
.bindMemory(to: Element.self, capacity: 1)

start.moveInitialize(from: start + 1, count: offset)
(start + offset).initialize(to: item)
let prefix = c &- offset
start.moveInitialize(from: start + 1, count: prefix)
(start + prefix).initialize(to: item)
}

/// Insert `child` at `offset`. There must be enough free space in the node
Expand Down Expand Up @@ -66,18 +67,19 @@ extension _Node.UnsafeHandle {
@inlinable
internal func _removeItem(at offset: Int) -> Element {
assertMutable()
let count = itemCount
assert(offset >= 0 && offset < count)
let c = itemCount
assert(offset >= 0 && offset < c)
let stride = MemoryLayout<Element>.stride
bytesFree &+= stride

let start = _memory
.advanced(by: byteCapacity &- stride &* count)
.advanced(by: byteCapacity &- stride &* c)
.assumingMemoryBound(to: Element.self)

let q = start + offset
let prefix = c &- 1 &- offset
let q = start + prefix
let item = q.move()
(start + 1).moveInitialize(from: start, count: offset)
(start + 1).moveInitialize(from: start, count: prefix)
return item
}

Expand Down Expand Up @@ -249,19 +251,19 @@ extension _Node {
insertItem((key, value), at: offset, bucket)
return nil
case .newCollision(let bucket, let offset):
let hash2 = read { _Hash($0[item: offset].key) }
if hash == hash2, hasSingletonItem {
let existingHash = read { _Hash($0[item: offset].key) }
if hash == existingHash, hasSingletonItem {
// Convert current node to a collision node.
ensureUnique(isUnique: isUnique, withFreeSpace: Self.spaceForNewItem)
update { $0.collisionCount = 1 }
insertItem((key, value), at: 0, .invalid)
insertItem((key, value), at: 1, .invalid)
} else {
ensureUnique(isUnique: isUnique, withFreeSpace: Self.spaceForNewCollision)
let item2 = removeItem(at: offset, bucket)
let existing = removeItem(at: offset, bucket)
let node = _Node(
level: level.descend(),
item1: (key, value), hash,
item2: item2, hash2)
item1: existing, existingHash,
item2: (key, value), hash)
insertChild(node, bucket)
}
return nil
Expand Down
8 changes: 5 additions & 3 deletions Sources/PersistentCollections/Node/_Node+Storage.swift
Expand Up @@ -42,7 +42,7 @@ extension _Node {
deinit {
UnsafeHandle.update(self) { handle in
handle._children.deinitialize()
handle._items.deinitialize()
handle.reverseItems.deinitialize()
}
}
}
Expand Down Expand Up @@ -162,7 +162,8 @@ extension _Node {
let i = target._children.initialize(fromContentsOf: source._children)
assert(i == target.childCount)

let j = target._items.initialize(fromContentsOf: source._items)
let j = target.reverseItems
.initialize(fromContentsOf: source.reverseItems)
assert(j == target.itemCount)
}
}
Expand All @@ -185,7 +186,8 @@ extension _Node {
let i = target._children.moveInitialize(fromContentsOf: source._children)
assert(i == target.childCount)

let j = target._items.moveInitialize(fromContentsOf: source._items)
let j = target.reverseItems
.moveInitialize(fromContentsOf: source.reverseItems)
assert(j == target.itemCount)
source.itemMap = .empty
source.childMap = .empty
Expand Down
Expand Up @@ -171,15 +171,15 @@ extension _Node.UnsafeHandle {
}

@inlinable
internal var _items: UnsafeMutableBufferPointer<Element> {
internal var reverseItems: UnsafeMutableBufferPointer<Element> {
msteindorfer marked this conversation as resolved.
Show resolved Hide resolved
let c = itemCount
return UnsafeMutableBufferPointer(start: _itemsEnd - c, count: c)
}

@inlinable
internal func itemPtr(at offset: Int) -> UnsafeMutablePointer<Element> {
assert(offset >= 0 && offset < itemCount)
return _itemsEnd.advanced(by: -(itemCount &- offset))
return _itemsEnd.advanced(by: -1 &- offset)
}

@inlinable
Expand Down
4 changes: 3 additions & 1 deletion Sources/PersistentCollections/Node/_Node.swift
Expand Up @@ -38,10 +38,12 @@ internal struct _RawNode {
/// The storage is arranged as shown below.
///
/// ┌───┬───┬───┬───┬───┬──────────────────┬───┬───┬───┬───┐
/// │ 0 │ 1 │ 2 │ 3 │ 4 │→ free space ←│ 0123
/// │ 0 │ 1 │ 2 │ 3 │ 4 │→ free space ←│ 3210
/// └───┴───┴───┴───┴───┴──────────────────┴───┴───┴───┴───┘
/// children items
///
msteindorfer marked this conversation as resolved.
Show resolved Hide resolved
/// Note that the region for items grows downwards from the end.
///
/// Two 32-bit bitmaps are used to associate each child and item with their
/// position in the hash table. The bucket occupied by the *n*th child in the
/// buffer above is identified by position of the *n*th true bit in the child
Expand Down
Expand Up @@ -18,7 +18,7 @@ extension PersistentDictionary: Sequence {
// before any items within children.

@usableFromInline
typealias _ItemBuffer = UnsafeBufferPointer<Element>
typealias _ItemBuffer = ReversedCollection<UnsafeMutableBufferPointer<Element>>

@usableFromInline
typealias _ChildBuffer = UnsafeBufferPointer<_Node>
Expand All @@ -44,7 +44,7 @@ extension PersistentDictionary: Sequence {
// FIXME: This is illegal, as it escapes pointers to _root contents
// outside the closure passed to `read`. :-(
self._itemIterator = _root.read {
$0.hasItems ? $0._items.makeIterator() : nil
$0.hasItems ? $0.reverseItems.reversed().makeIterator() : nil
}
self._pathTop = _root.read {
$0.hasChildren ? $0._children.makeIterator() : nil
Expand Down Expand Up @@ -84,7 +84,7 @@ extension PersistentDictionary.Iterator: IteratorProtocol {
_pathTop = nextNode.read { $0._children.makeIterator() } // 💥ILLEGAL
}
if nextNode.read({ $0.hasItems }) {
_itemIterator = nextNode.read { $0._items.makeIterator() } // 💥ILLEGAL
_itemIterator = nextNode.read { $0.reverseItems.reversed().makeIterator() } // 💥ILLEGAL
return _itemIterator!.next()
}
}
Expand Down