From 54b1451042679692c73fcd72b7a49430b3ed5f68 Mon Sep 17 00:00:00 2001 From: Brennan Saeta Date: Fri, 4 Sep 2020 12:23:57 -0700 Subject: [PATCH 1/2] Move DoubleEndedBuffer to a new file. This is step 1 in rewriting Deque to be more general and flexible. --- Sources/PenguinStructures/Deque.swift | 224 ----------------- .../PenguinStructures/DoubleEndedBuffer.swift | 237 ++++++++++++++++++ 2 files changed, 237 insertions(+), 224 deletions(-) create mode 100644 Sources/PenguinStructures/DoubleEndedBuffer.swift diff --git a/Sources/PenguinStructures/Deque.swift b/Sources/PenguinStructures/Deque.swift index 61b304f8..92288025 100644 --- a/Sources/PenguinStructures/Deque.swift +++ b/Sources/PenguinStructures/Deque.swift @@ -204,227 +204,3 @@ extension Deque: HierarchicalCollection { return nil } } - -/// A fixed-size, contiguous collection allowing additions and removals at either end (space -/// permitting). -/// -/// Beware: unbalanced pushes/pops to either the front or the back will result in the effective -/// working size to be diminished. If you would like this to be managed for you automatically, -/// please use a `Deque`. -/// -/// - SeeAlso: `Deque` -public struct DoubleEndedBuffer { - private var buff: ManagedBuffer - - /// Allocate with a given capacity and insertion `initialPolicy`. - /// - /// - Parameter capacity: The capacity of the buffer. - /// - Parameter initialPolicy: The policy for where initial values should be inserted into the - /// buffer. Note: asymmetric pushes/pops to front/back will cause the portion of the consumed - /// buffer to drift. If you need management to occur automatically, please use a Deque. - public init(capacity: Int, with initialPolicy: DoubleEndedAllocationPolicy) { - assert(capacity > 3) - buff = DoubleEndedBufferImpl.create(minimumCapacity: capacity) { buff in - switch initialPolicy { - case .beginning: - return DoubleEndedHeader(start: 0, end: 0) - case .middle: - let approxMiddle = buff.capacity / 2 - return DoubleEndedHeader(start: approxMiddle, end: approxMiddle) - case .end: - return DoubleEndedHeader(start: buff.capacity, end: buff.capacity) - } - } - } - - /// True if no elements are contained within the data structure, false otherwise. - public var isEmpty: Bool { - buff.header.start == buff.header.end - } - - /// Returns the number of elements contained within `self`. - public var count: Int { - buff.header.end - buff.header.start - } - - /// Returns the capacity of `self`. - public var capacity: Int { - buff.capacity - } - - /// True iff there is available space at the beginning of the buffer. - public var canPushFront: Bool { - buff.header.start != 0 - } - - /// True iff there is available space at the end of the buffer. - public var canPushBack: Bool { - buff.header.end < buff.capacity - } - - /// Add elem to the back of the buffer. - /// - /// - Precondition: `canPushBack`. - public mutating func pushBack(_ elem: T) { - precondition(canPushBack, "Cannot pushBack!") - ensureBuffIsUniquelyReferenced() - buff.withUnsafeMutablePointerToElements { buffP in - let offset = buffP.advanced(by: buff.header.end) - offset.initialize(to: elem) - } - buff.header.end += 1 - } - - /// Removes and returns the element at the back, reducing `self`'s count by one. - /// - /// - Precondition: !isEmpty - public mutating func popBack() -> T { - precondition(!isEmpty, "Cannot popBack from empty buffer!") - ensureBuffIsUniquelyReferenced() - buff.header.end -= 1 - return buff.withUnsafeMutablePointerToElements { buffP in - buffP.advanced(by: buff.header.end).move() - } - } - - /// Adds elem to the front of the buffer. - /// - /// - Precondition: `canPushFront`. - public mutating func pushFront(_ elem: T) { - precondition(canPushFront, "Cannot pushFront!") - ensureBuffIsUniquelyReferenced() - buff.header.start -= 1 - buff.withUnsafeMutablePointerToElements { buffP in - let offset = buffP.advanced(by: buff.header.start) - offset.initialize(to: elem) - } - } - - /// Removes and returns the element at the front, reducing `self`'s count by one. - public mutating func popFront() -> T { - precondition(!isEmpty, "Cannot popFront from empty buffer!") - ensureBuffIsUniquelyReferenced() - buff.header.start += 1 - return buff.withUnsafeMutablePointerToElements { buffP in - buffP.advanced(by: buff.header.start - 1).move() - } - } - - /// Makes a copy of the backing buffer if it's not uniquely referenced; does nothing otherwise. - private mutating func ensureBuffIsUniquelyReferenced() { - if !isKnownUniquelyReferenced(&buff) { - // make a copy of the backing store. - let copy = DoubleEndedBufferImpl.create(minimumCapacity: buff.capacity) { copy in - return buff.header - } - copy.withUnsafeMutablePointerToElements { copyP in - buff.withUnsafeMutablePointerToElements { buffP in - let copyOffset = copyP.advanced(by: buff.header.start) - let buffOffset = UnsafePointer(buffP).advanced(by: buff.header.start) - copyOffset.initialize(from: buffOffset, count: buff.header.end - buff.header.start) - } - } - self.buff = copy - } - } - - /// Explicitly reallocate the backing buffer. - public mutating func reallocate( - newCapacity: Int, - with initialPolicy: DoubleEndedAllocationPolicy - ) { - assert(newCapacity >= count) - let newHeader: DoubleEndedHeader - switch initialPolicy { - case .beginning: - newHeader = DoubleEndedHeader(start: 0, end: count) - case .middle: - let newStart = (newCapacity - count) / 2 - newHeader = DoubleEndedHeader(start: newStart, end: newStart + count) - case .end: - newHeader = DoubleEndedHeader(start: newCapacity - count, end: newCapacity) - } - let copy = DoubleEndedBufferImpl.create(minimumCapacity: newCapacity) { copy in - newHeader - } - if !isKnownUniquelyReferenced(&buff) { - // Don't touch existing one; must make a copy of buffer contents. - copy.withUnsafeMutablePointerToElements { copyP in - buff.withUnsafeMutablePointerToElements { buffP in - let copyOffset = copyP.advanced(by: copy.header.start) - let buffOffset = UnsafePointer(buffP).advanced(by: buff.header.start) - copyOffset.initialize(from: buffOffset, count: count) - } - } - } else { - // Move values out of existing buffer into new buffer. - copy.withUnsafeMutablePointerToElements { copyP in - buff.withUnsafeMutablePointerToElements { buffP in - let copyOffset = copyP.advanced(by: copy.header.start) - let buffOffset = buffP.advanced(by: buff.header.start) - copyOffset.moveInitialize(from: buffOffset, count: count) - } - } - buff.header.end = buff.header.start // don't deinitialize uninitialized memory. - } - self.buff = copy - } -} - -extension DoubleEndedBuffer: Collection { - public var startIndex: Int { buff.header.start } - public var endIndex: Int { buff.header.end } - public func index(after: Int) -> Int { after + 1 } - - public subscript(index: Int) -> T { - get { - assert(index >= buff.header.start) - assert(index < buff.header.end) - return buff.withUnsafeMutablePointerToElements { $0[index] } - } - _modify { - assert(index >= buff.header.start) - assert(index < buff.header.end) - ensureBuffIsUniquelyReferenced() - var tmp = buff.withUnsafeMutablePointerToElements { $0.advanced(by: index).move() } - // Ensure we re-initialize the memory! - defer { - buff.withUnsafeMutablePointerToElements { - $0.advanced(by: index).initialize(to: tmp) - } - } - yield &tmp - } - } -} - -/// Describes where the initial insertions into a buffer should go. -/// -/// - SeeAlso: `DoubleEndedBuffer` -public enum DoubleEndedAllocationPolicy { - /// Begin allocating elements at the beginning of the buffer. - case beginning - /// Begin allocating in the middle of the buffer. - case middle - /// Begin allocating at the end of the buffer. - case end -} - -private struct DoubleEndedHeader { - // TODO: use smaller `Int`s to save memory. - /// The first index with valid data. - var start: Int - /// The index one after the last index with valid data. - var end: Int -} - -private class DoubleEndedBufferImpl: ManagedBuffer { - deinit { - if header.end != header.start { - withUnsafeMutablePointerToElements { elems in - let base = elems.advanced(by: header.start) - base.deinitialize(count: header.end - header.start) - } - } - } -} diff --git a/Sources/PenguinStructures/DoubleEndedBuffer.swift b/Sources/PenguinStructures/DoubleEndedBuffer.swift new file mode 100644 index 00000000..9a067553 --- /dev/null +++ b/Sources/PenguinStructures/DoubleEndedBuffer.swift @@ -0,0 +1,237 @@ +// Copyright 2020 Penguin Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// A fixed-size, contiguous collection allowing additions and removals at either end (space +/// permitting). +/// +/// Beware: unbalanced pushes/pops to either the front or the back will result in the effective +/// working size to be diminished. If you would like this to be managed for you automatically, +/// please use a `Deque`. +/// +/// - SeeAlso: `Deque` +public struct DoubleEndedBuffer { + private var buff: ManagedBuffer + + /// Allocate with a given capacity and insertion `initialPolicy`. + /// + /// - Parameter capacity: The capacity of the buffer. + /// - Parameter initialPolicy: The policy for where initial values should be inserted into the + /// buffer. Note: asymmetric pushes/pops to front/back will cause the portion of the consumed + /// buffer to drift. If you need management to occur automatically, please use a Deque. + public init(capacity: Int, with initialPolicy: DoubleEndedAllocationPolicy) { + assert(capacity > 3) + buff = DoubleEndedBufferImpl.create(minimumCapacity: capacity) { buff in + switch initialPolicy { + case .beginning: + return DoubleEndedHeader(start: 0, end: 0) + case .middle: + let approxMiddle = buff.capacity / 2 + return DoubleEndedHeader(start: approxMiddle, end: approxMiddle) + case .end: + return DoubleEndedHeader(start: buff.capacity, end: buff.capacity) + } + } + } + + /// True if no elements are contained within the data structure, false otherwise. + public var isEmpty: Bool { + buff.header.start == buff.header.end + } + + /// Returns the number of elements contained within `self`. + public var count: Int { + buff.header.end - buff.header.start + } + + /// Returns the capacity of `self`. + public var capacity: Int { + buff.capacity + } + + /// True iff there is available space at the beginning of the buffer. + public var canPushFront: Bool { + buff.header.start != 0 + } + + /// True iff there is available space at the end of the buffer. + public var canPushBack: Bool { + buff.header.end < buff.capacity + } + + /// Add elem to the back of the buffer. + /// + /// - Precondition: `canPushBack`. + public mutating func pushBack(_ elem: T) { + precondition(canPushBack, "Cannot pushBack!") + ensureBuffIsUniquelyReferenced() + buff.withUnsafeMutablePointerToElements { buffP in + let offset = buffP.advanced(by: buff.header.end) + offset.initialize(to: elem) + } + buff.header.end += 1 + } + + /// Removes and returns the element at the back, reducing `self`'s count by one. + /// + /// - Precondition: !isEmpty + public mutating func popBack() -> T { + precondition(!isEmpty, "Cannot popBack from empty buffer!") + ensureBuffIsUniquelyReferenced() + buff.header.end -= 1 + return buff.withUnsafeMutablePointerToElements { buffP in + buffP.advanced(by: buff.header.end).move() + } + } + + /// Adds elem to the front of the buffer. + /// + /// - Precondition: `canPushFront`. + public mutating func pushFront(_ elem: T) { + precondition(canPushFront, "Cannot pushFront!") + ensureBuffIsUniquelyReferenced() + buff.header.start -= 1 + buff.withUnsafeMutablePointerToElements { buffP in + let offset = buffP.advanced(by: buff.header.start) + offset.initialize(to: elem) + } + } + + /// Removes and returns the element at the front, reducing `self`'s count by one. + public mutating func popFront() -> T { + precondition(!isEmpty, "Cannot popFront from empty buffer!") + ensureBuffIsUniquelyReferenced() + buff.header.start += 1 + return buff.withUnsafeMutablePointerToElements { buffP in + buffP.advanced(by: buff.header.start - 1).move() + } + } + + /// Makes a copy of the backing buffer if it's not uniquely referenced; does nothing otherwise. + private mutating func ensureBuffIsUniquelyReferenced() { + if !isKnownUniquelyReferenced(&buff) { + // make a copy of the backing store. + let copy = DoubleEndedBufferImpl.create(minimumCapacity: buff.capacity) { copy in + return buff.header + } + copy.withUnsafeMutablePointerToElements { copyP in + buff.withUnsafeMutablePointerToElements { buffP in + let copyOffset = copyP.advanced(by: buff.header.start) + let buffOffset = UnsafePointer(buffP).advanced(by: buff.header.start) + copyOffset.initialize(from: buffOffset, count: buff.header.end - buff.header.start) + } + } + self.buff = copy + } + } + + /// Explicitly reallocate the backing buffer. + public mutating func reallocate( + newCapacity: Int, + with initialPolicy: DoubleEndedAllocationPolicy + ) { + assert(newCapacity >= count) + let newHeader: DoubleEndedHeader + switch initialPolicy { + case .beginning: + newHeader = DoubleEndedHeader(start: 0, end: count) + case .middle: + let newStart = (newCapacity - count) / 2 + newHeader = DoubleEndedHeader(start: newStart, end: newStart + count) + case .end: + newHeader = DoubleEndedHeader(start: newCapacity - count, end: newCapacity) + } + let copy = DoubleEndedBufferImpl.create(minimumCapacity: newCapacity) { copy in + newHeader + } + if !isKnownUniquelyReferenced(&buff) { + // Don't touch existing one; must make a copy of buffer contents. + copy.withUnsafeMutablePointerToElements { copyP in + buff.withUnsafeMutablePointerToElements { buffP in + let copyOffset = copyP.advanced(by: copy.header.start) + let buffOffset = UnsafePointer(buffP).advanced(by: buff.header.start) + copyOffset.initialize(from: buffOffset, count: count) + } + } + } else { + // Move values out of existing buffer into new buffer. + copy.withUnsafeMutablePointerToElements { copyP in + buff.withUnsafeMutablePointerToElements { buffP in + let copyOffset = copyP.advanced(by: copy.header.start) + let buffOffset = buffP.advanced(by: buff.header.start) + copyOffset.moveInitialize(from: buffOffset, count: count) + } + } + buff.header.end = buff.header.start // don't deinitialize uninitialized memory. + } + self.buff = copy + } +} + +extension DoubleEndedBuffer: Collection { + public var startIndex: Int { buff.header.start } + public var endIndex: Int { buff.header.end } + public func index(after: Int) -> Int { after + 1 } + + public subscript(index: Int) -> T { + get { + assert(index >= buff.header.start) + assert(index < buff.header.end) + return buff.withUnsafeMutablePointerToElements { $0[index] } + } + _modify { + assert(index >= buff.header.start) + assert(index < buff.header.end) + ensureBuffIsUniquelyReferenced() + var tmp = buff.withUnsafeMutablePointerToElements { $0.advanced(by: index).move() } + // Ensure we re-initialize the memory! + defer { + buff.withUnsafeMutablePointerToElements { + $0.advanced(by: index).initialize(to: tmp) + } + } + yield &tmp + } + } +} + +/// Describes where the initial insertions into a buffer should go. +/// +/// - SeeAlso: `DoubleEndedBuffer` +public enum DoubleEndedAllocationPolicy { + /// Begin allocating elements at the beginning of the buffer. + case beginning + /// Begin allocating in the middle of the buffer. + case middle + /// Begin allocating at the end of the buffer. + case end +} + +internal struct DoubleEndedHeader { + // TODO: use smaller `Int`s to save memory. + /// The first index with valid data. + var start: Int + /// The index one after the last index with valid data. + var end: Int +} + +private class DoubleEndedBufferImpl: ManagedBuffer { + deinit { + if header.end != header.start { + withUnsafeMutablePointerToElements { elems in + let base = elems.advanced(by: header.start) + base.deinitialize(count: header.end - header.start) + } + } + } +} From aeb093e37c2b15b60f6409fc7eec8092dfe99a78 Mon Sep 17 00:00:00 2001 From: Brennan Saeta Date: Mon, 7 Sep 2020 21:05:51 -0700 Subject: [PATCH 2/2] Rewrite `Deque`. The previous implementation did not appropriately conform to the standard Collection protocols, and additionally had extra reference counting operations. This new Deque implementation is designed to be more flexible and easy to use without sacrificing performance. --- Sources/PenguinStructures/Deque.swift | 484 ++++++++++++++---- .../DequeInternalTests.swift | 36 ++ Tests/PenguinStructuresTests/DequeTests.swift | 12 +- .../XCTestManifests.swift | 1 + 4 files changed, 412 insertions(+), 121 deletions(-) create mode 100644 Tests/PenguinStructuresTests/DequeInternalTests.swift diff --git a/Sources/PenguinStructures/Deque.swift b/Sources/PenguinStructures/Deque.swift index 92288025..416183d6 100644 --- a/Sources/PenguinStructures/Deque.swift +++ b/Sources/PenguinStructures/Deque.swift @@ -28,117 +28,376 @@ public protocol Queue { mutating func push(_ element: Element) } -// MARK: - Deques +// MARK: - Deque -/// A dynamically-sized double-ended queue that allows pushing and popping at both the front and the -/// back. +/// A dynamically-sized queue with efficient additions and removals at both the beginning and the end. +/// +/// Deque's have stable indices, such that pushing and popping elements do not invalidate indices for +/// unaffected elements. public struct Deque { - /// A block of data - private typealias Block = DoubleEndedBuffer + private var spine: Spine +} + +extension Deque { + /// The number of bits used to encode the per-page element offset. + private static var maxPerBlockElementBits: UInt { 13 } + /// The hard-coded size of a block, in bytes. + private static var blockSize: UInt { 4096 } + /// A mask to extract the offset into a block. + private static var blockOffsetMask: UInt { (1 << maxPerBlockElementBits) - 1 } + /// A mask to extract the page identifier. + private static var blockIDMask: UInt { ~blockOffsetMask } + /// The number of bits used to represent block IDs. + private static var blockIDBitCount: UInt { UInt(UInt.bitWidth) - maxPerBlockElementBits - 1 } + /// Maximum block ID, and can also be used as a bitmask for blockIDs. + internal static var maxBlockID: UInt { (1 << blockIDBitCount) - 1 } + /// The number of elements per block. + private static var elementsPerBlock: UInt { Self.blockSize / UInt(MemoryLayout.stride) } - /// The elements contained within the data structure. + /// Prints out sizes for internal data structures. /// - /// Invariant: buff is never empty (it always contains at least one (nested) Block). - private var buff: DoubleEndedBuffer + /// When ensuring Deque works as expected for your platform, this function will print to stdout the sizes of internal + /// data structures. + public static func _printDequeStaticInternalConfiguration() { + print(""" + Deque configuration: + - maxPerBlockElementBits: \(maxPerBlockElementBits) + - blockSize: \(blockSize) bytes + - blockOffsetMask: \(String(blockOffsetMask, radix: 2)) + - blockIDMask: \(String(blockIDMask, radix: 2)) + - blockIDBitCount: \(blockIDBitCount) + - maxBlockID: \(maxBlockID) + - elementsPerBlock: \(elementsPerBlock) + """) + } - /// The number of elements contained within `self`. - public private(set) var count: Int + /// Halts the program if the hard-coded configuration values are inconsistent with each other. + private func assertConstantInvariants() { + assert(Self.blockSize == (1 << (Self.maxPerBlockElementBits - 1)), "The block size must be exactly 2^(maxPerBlockElementBits - 1)") + assert(Self.maxBlockID & (Self.maxBlockID + 1) == 0, "The maximum blockID must be one less than a power of 2 for fast masking.") + } - /// Creates an empty Deque. + /// A partially- or completely-filled block of elements allocated in page-size increments. + internal typealias Block = UnsafeMutablePointer + + // TODO: Make type `Index` exist outside of `Deque` to allow nested collections with intertwined indices (e.g. adjacency lists). + /// A position into the Deque. + public struct Index: Equatable, Hashable, Comparable { + /// Storage for the packed representation of the offset. + internal var storage: UInt + + /// The offset into a block of elements. + internal var blockOffset: UInt { storage & Deque.blockOffsetMask } + /// A stable identifier for a block of storage. + internal var blockID: UInt { ((storage & Deque.blockIDMask) >> maxPerBlockElementBits) & Deque.maxBlockID } + + /// Returns a Boolean value indicating whether the value of the first argument is less than that of the second argument. + public static func < (lhs: Self, rhs: Self) -> Bool { lhs.storage < rhs.storage } // TODO: Is this right? + } + + /// Information on the layout of data within the Deque. + internal struct Metadata { + /// The position of the first element (if non-empty). + var start: Index + /// The position one greater than the last valid index. + var end: Index + /// The offset to use to index into the spine. + var blockOffset: UInt + } + + /// An ordered collection of pointers to buffers containing Elements. /// - /// - Parameter bufferSize: The capacity (in terms of elements) of the initial Deque. If - /// unspecified, `Deque` uses a heuristic to pick a value, tuned for performance. - public init(initialCapacity: Int? = nil) { - let blockSize: Int - if let capacity = initialCapacity { - blockSize = capacity - } else { - if MemoryLayout.stride < 256 { - // ~4k pages; minus the overheads. - blockSize = (4096 - MemoryLayout.size - 8) / MemoryLayout.stride + /// A Deque is composed of a hierarchy of two buffers: the first is the spine, + internal final class Spine: ManagedBuffer { + + /// The number of elements in `self`. + public var count: Int { + // Note: this calculation works even if there isn't a full page of elements, and has the benefit + // of being fully branchless. + let startBlockElems = Deque.elementsPerBlock - header.start.blockOffset + let endBlockElems = header.end.blockOffset + let middleBlocks = (endBlockOffset - startBlockOffset - 1) * Int(bitPattern: Deque.elementsPerBlock) // Can be negative! + return Int(bitPattern: startBlockElems + endBlockElems) + middleBlocks + } + + /// true if `self` contains an element; false otherwise. + public var isEmpty: Bool { header.start == header.end } + + public func index(after i: Index) -> Index { + var next = i + next.storage += 1 + if _slowPath(next.blockOffset == Deque.elementsPerBlock) { + next = Index(blockOffset: 0, blockID: (next.blockID + 1) & Deque.maxBlockID) + } + return next + } + + public func index(before i: Index) -> Index { + var next = i + if _slowPath(i.blockOffset == 0) { + next = Index(blockOffset: Deque.elementsPerBlock - 1, blockID: (i.blockID - 1) & Deque.maxBlockID) } else { - // Store at least 16 elements per block. - blockSize = 16 + next.storage -= 1 } + return next + } + + /// The index into `self`'s element pointer for the start block. + internal var startBlockOffset: Int { + spineOffsetForIndex(header.start) + } + + /// The index into `self`'s element pointer for the end block. + internal var endBlockOffset: Int { + spineOffsetForIndex(header.end) } - buff = DoubleEndedBuffer(capacity: 16, with: .middle) - buff.pushBack(Block(capacity: blockSize, with: .middle)) - count = 0 - } - /// True iff no values are contained in `self. - public var isEmpty: Bool { count == 0 } + /// Returns the offset into the elements of `self` corresponding to `index`'s `blockID`. + internal func spineOffsetForIndex(_ index: Index) -> Int { + let base = index.blockID + header.blockOffset + return Int(bitPattern: base & Deque.maxBlockID) // Use bitPattern to avoid extra branches / traps. + } - private mutating func reallocateBuff() { - if buff.count * 2 < buff.capacity { - // Reallocate to the same size to avoid taking too much memory. - buff.reallocate(newCapacity: buff.capacity, with: .middle) - } else { - buff.reallocate(newCapacity: buff.capacity * 2, with: .middle) + /// Allocates a new empty spine. + class func createEmpty(blockCount: UInt = 10) -> Spine { + let buff = Spine.create(minimumCapacity: Int(blockCount)) { buff in + buff.withUnsafeMutablePointerToElements { + $0.initialize(repeating: nil, count: buff.capacity) + } + return Metadata(start: Index(storage: 0), end: Index(storage: 0), blockOffset: blockCount / 2) + } + return buff as! Spine + } + + func deepClone() -> Spine { + let oldCapacity = capacity + let oldMetadata = header + let oldStartOffset = spineOffsetForIndex(header.start) + let oldEndOffset = spineOffsetForIndex(header.end) + let s = Spine.create(minimumCapacity: oldCapacity) { _ in oldMetadata } + withUnsafeMutablePointerToElements { old in + s.withUnsafeMutablePointerToElements { new in + if oldStartOffset > 0 { + new.initialize(repeating: nil, count: oldStartOffset) + } + if oldStartOffset == oldEndOffset { + // Initialize only the relevant portions of memory. + if old[oldStartOffset] != nil { + let newPage = Block.allocate(capacity: Int(bitPattern: Deque.elementsPerBlock)) + let blockOffset = oldMetadata.start.blockOffset + let blockCount = oldMetadata.end.blockOffset - blockOffset + (newPage + blockOffset).initialize(from: old[oldStartOffset]! + blockOffset, count: blockCount) + new[oldStartOffset] = newPage + } else { new[oldStartOffset] = nil } + } else { + // Copy both the first & last page. + let newStartPage = Block.allocate(capacity: Int(bitPattern: Deque.elementsPerBlock)) + let startBlockOffset = oldMetadata.start.blockOffset + (newStartPage + startBlockOffset).initialize( + from: old[oldStartOffset]! + startBlockOffset, + count: Deque.elementsPerBlock - startBlockOffset) + new[oldStartOffset] = newStartPage + if old[oldEndOffset] != nil { + let newEndPage = Block.allocate(capacity: Int(bitPattern: Deque.elementsPerBlock)) + newEndPage.initialize(from: old[oldEndOffset], count: oldMetadata.end.blockOffset) + new[oldEndOffset] = newEndPage + } else { new[oldEndOffset] = nil } + // Copy all intermediate pages (if any). + if oldStartOffset + 1 < oldEndOffset - 1 { + for i in (oldStartOffset + 1)...(oldEndOffset - 1) { + // Make a copy of the page. + let newBlock = Block.allocate(capacity: Int(bitPattern: Deque.elementsPerBlock)) + newBlock.initialize(from: old[i]!, count: Int(bitPattern: Deque.elementsPerBlock)) + new[i] = newBlock + } + } + } + if oldEndOffset + 1 < s.capacity { + (new + oldEndOffset + 1).initialize(repeating: nil, count: s.capacity - oldEndOffset - 1) + } + } + } + return s as! Spine } } +} - /// Add `elem` to the back of `self`. - public mutating func pushBack(_ elem: Element) { - count += 1 - if buff[buff.endIndex - 1].canPushBack { - buff[buff.endIndex - 1].pushBack(elem) - } else { - if buff[buff.endIndex - 1].isEmpty { - // Re-use the previous buffer. - buff[buff.endIndex - 1].pushFront(elem) +extension Deque { + /// Ensures a particular block is allocated and returns the block pointer. + internal mutating func ensureAllocatedBlock(at i: Index) -> Block { + var spineOffset = spine.spineOffsetForIndex(i) + // First, check to ensure the spineOffset is not out of bounds. + if _slowPath(spineOffset < 0 || spineOffset >= spine.capacity) { + // Must modify the spine. + let startOffset = spine.spineOffsetForIndex(startIndex) + let endOffset = spine.spineOffsetForIndex(endIndex) + let occupiedSlots = endOffset - startOffset + if occupiedSlots + 1 < spine.capacity { + // Re-center and avoid reallocating a larger spine. + let newStartOffset = (spine.capacity - occupiedSlots) / 2 + let newEndOffset = newStartOffset + occupiedSlots + assert(newStartOffset > 0) + assert(newEndOffset < spine.capacity) + assert(newStartOffset != startOffset) + spine.withUnsafeMutablePointerToElements { elems in + let newStart = elems + newStartOffset + let oldStart = elems + startOffset + newStart.assign(from: UnsafePointer(oldStart), count: occupiedSlots) + if newStartOffset > startOffset { + oldStart.assign(repeating: nil, count: newStartOffset - startOffset) + } else { + let newEnd = elems + newEndOffset + newEnd.assign(repeating: nil, count: endOffset - newEndOffset) + } + } + let newOffset = Int(spine.header.blockOffset) + (newStartOffset - startOffset) + spine.header.blockOffset = UInt(newOffset) & Deque.maxBlockID } else { - // Allocate a new buffer. - var newBlock = Block(capacity: buff[buff.endIndex - 1].capacity, with: .beginning) - newBlock.pushBack(elem) - if !buff.canPushBack { - reallocateBuff() + // Must allocate a new, larger spine; no need to copy the data blocks. + let newSpine = Spine.create(minimumCapacity: 2 * spine.capacity) { newSpine in + // Re-center while we're at it. + let newStartOffset = (newSpine.capacity - occupiedSlots) / 2 + spine.withUnsafeMutablePointerToElements { oldBuff in + newSpine.withUnsafeMutablePointerToElements { newBuff in + newBuff.initialize(repeating: nil, count: newStartOffset) + let newStart = newBuff + newStartOffset + newStart.initialize(from: UnsafePointer(oldBuff + startOffset), count: occupiedSlots) + (newStart + occupiedSlots).initialize(repeating: nil, count: newSpine.capacity - occupiedSlots - newStartOffset) + } + } + let newBlockOffset = Int(startIndex.blockID) + newStartOffset + return Metadata(start: startIndex, end: endIndex, blockOffset: UInt(bitPattern: newBlockOffset) & Deque.maxBlockID) } - buff.pushBack(newBlock) + spine = newSpine as! Spine + } + spineOffset = spine.spineOffsetForIndex(i) + } + // Next check if the block is allocated. + return spine.withUnsafeMutablePointerToElements { elems in + if _fastPath(elems[spineOffset] != nil) { return elems[spineOffset]! } + // Check to see if we have an empty allocated block available to reuse. + let beforeStart = spine.spineOffsetForIndex(startIndex) - 1 + if beforeStart >= 0 && elems[beforeStart] != nil { + let reuseBlock = elems[beforeStart]! + elems[spineOffset] = reuseBlock + elems[beforeStart] = nil + return reuseBlock + } + let afterEnd = spine.spineOffsetForIndex(endIndex) + 1 + if afterEnd < spine.capacity && elems[afterEnd] != nil { + let reuseBlock = elems[afterEnd]! + elems[afterEnd] = nil + elems[spineOffset] = reuseBlock + return reuseBlock } + let newBlock = Block.allocate(capacity: Int(bitPattern: Deque.elementsPerBlock)) + elems[spineOffset] = newBlock + return newBlock } } + /// Indicates a given block should be considered empty and optionally deallocated. + /// + /// In order to avoid degenerate performance cases of allocating and deallocating a block of memory + /// (e.g. repeatedly pushing and popping a single element right at the page boundary), a block may be + /// lazily deallocated. In order to take advantage of this performance optimization, blocks should be + /// allocated with `ensureAllocatedBlock` and deallocated with `markEmptyBlock`. + internal mutating func markEmptyBlock(at spineOffset: Int) { + // TODO: Implement this optimization. + } + + /// Ensure that `self` holds uniquely-referenced storage, copying its memory if necessary. + internal mutating func ensureUniqueStorage() { + if !isKnownUniquelyReferenced(&spine) { + spine = spine.deepClone() + } + } +} + +extension Deque: Collection, BidirectionalCollection { + public var startIndex: Index { spine.header.start } + public var endIndex: Index { spine.header.end } + public var count: Int { spine.count } + public var isEmpty: Bool { spine.isEmpty } + public func index(after i: Index) -> Index { spine.index(after: i) } + public func index(before i: Index) -> Index { spine.index(before: i) } + + public subscript(i: Index) -> Element { + // TODO: Ensure `i` is a valid index! + let spineOffset = spine.spineOffsetForIndex(i) + return spine.withUnsafeMutablePointerToElements { $0[spineOffset]![Int(i.blockOffset)] } + } +} + +// TODO: Conform Deque to RandomAccessCollection, MutableCollection, and RangeReplaceableCollection. + +extension Deque { + + /// Creates an empty Deque. + public init() { + self.spine = Spine.createEmpty() + } + + /// Creates an instance with the same elements as `contents`. + public init(_ contents: Contents) where Contents.Element == Element { + self.init() + for e in contents { + pushBack(e) + } + } + + /// Add `elem` to the back of `self`. + public mutating func pushBack(_ elem: Element) { + ensureUniqueStorage() + let i = endIndex + let block = ensureAllocatedBlock(at: i) + let position = block + Int(bitPattern: i.blockOffset) + position.initialize(to: elem) + spine.header.end = spine.index(after: i) + } + /// Removes and returns the element at the back, reducing `self`'s count by one. /// /// - Precondition: !isEmpty public mutating func popBack() -> Element { - assert(!isEmpty, "Cannot popBack from an empty Deque.") - count -= 1 - let tmp = buff[buff.endIndex - 1].popBack() - if buff[buff.endIndex - 1].isEmpty && buff.count > 1 { - _ = buff.popBack() + ensureUniqueStorage() + let i = spine.index(before: endIndex) + let offset = spine.spineOffsetForIndex(i) + let block = spine.withUnsafeMutablePointerToElements { $0[offset]! } + let position = block + Int(bitPattern: i.blockOffset) + let returnValue = position.move() + spine.header.end = i + if _slowPath(i.blockOffset == 0) { + markEmptyBlock(at: offset) } - return tmp + return returnValue } /// Adds `elem` to the front of `self`. public mutating func pushFront(_ elem: Element) { - count += 1 - if buff[buff.startIndex].canPushFront { - buff[buff.startIndex].pushFront(elem) - } else { - // Allocate a new buffer. - var newBlock = Block(capacity: buff[buff.startIndex].capacity, with: .end) - newBlock.pushFront(elem) - if !buff.canPushFront { - reallocateBuff() - } - buff.pushFront(newBlock) - } + ensureUniqueStorage() + let i = spine.index(before: startIndex) + let block = ensureAllocatedBlock(at: i) + let position = block + Int(bitPattern: i.blockOffset) + position.initialize(to: elem) + spine.header.start = i } /// Removes and returns the element at the front, reducing `self`'s count by one. /// /// - Precondition: !isEmpty public mutating func popFront() -> Element { - precondition(!isEmpty) - count -= 1 - let tmp = buff[buff.startIndex].popFront() - if buff[buff.startIndex].isEmpty && buff.count > 1 { - _ = buff.popFront() + ensureUniqueStorage() + let offset = spine.spineOffsetForIndex(startIndex) + let block = spine.withUnsafeMutablePointerToElements { $0[offset]! } + let position = block + Int(bitPattern: startIndex.blockOffset) + let returnValue = position.move() + let newStart = spine.index(after: startIndex) + if _slowPath(spine.spineOffsetForIndex(newStart) != offset) { + markEmptyBlock(at: offset) } - return tmp + spine.header.start = newStart + return returnValue } } @@ -155,52 +414,51 @@ extension Deque: Queue { } } -extension Deque: HierarchicalCollection { - public struct Cursor: Equatable, Comparable { - let outerIndex: Int - let innerIndex: Int +extension Deque.Index { + /// Creates an Index from a given blockOffset and blockID. + /// + /// - Precondition: blockOffset is a valid block offset. + internal init(blockOffset: UInt, blockID: UInt) { + precondition((blockOffset & Deque.blockOffsetMask) == blockOffset, "blockOffset \(blockOffset) is not a valid block offset") + storage = (blockID << Deque.maxPerBlockElementBits) | blockOffset + } +} - public static func < (lhs: Self, rhs: Self) -> Bool { - if lhs.outerIndex < rhs.outerIndex { return true } - if lhs.outerIndex > rhs.outerIndex { return false } - return lhs.innerIndex < rhs.innerIndex - } +extension Deque.Index: CustomStringConvertible { + public var description: String { + "Index(blockID: \(blockID), blockOffset: \(blockOffset))" } +} - /// Call `fn` for each element in the collection until `fn` returns false. - /// - /// - Parameter start: Start iterating at elements corresponding to this index. If nil, starts at - /// the beginning of the collection. - /// - Returns: a cursor into the data structure corresponding to the first element that returns - /// false. - @discardableResult - public func forEachWhile( - startingAt start: Cursor?, - _ fn: (Element) throws -> Bool - ) rethrows -> Cursor? { - let startPoint = - start - ?? Cursor( - outerIndex: buff.startIndex, - innerIndex: buff[buff.startIndex].startIndex) - - /// Start with potential partial first buffer. - for i in startPoint.innerIndex.. String { + if let block = block { + return "\(block)" + } else { + return "nil" } } - // Nested loops for remainder of data structure. - for outer in (startPoint.outerIndex + 1).. Self { + return lhs + Int(rhs) + } + + fileprivate func initialize(from: UnsafeMutablePointer?, count: UInt) { + initialize(from: UnsafePointer(from!), count: Int(bitPattern: count)) } } diff --git a/Tests/PenguinStructuresTests/DequeInternalTests.swift b/Tests/PenguinStructuresTests/DequeInternalTests.swift new file mode 100644 index 00000000..f94979a6 --- /dev/null +++ b/Tests/PenguinStructuresTests/DequeInternalTests.swift @@ -0,0 +1,36 @@ +// Copyright 2020 Penguin Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@testable import PenguinStructures +import XCTest + +final class DequeInternalTests: XCTestCase { + func testIndexComputedProperties() { + typealias Index = Deque.Index + + XCTAssertEqual(0, Index(storage: 0).blockOffset) + XCTAssertEqual(0, Index(storage: 0).blockID) + + XCTAssertEqual(5, Index(blockOffset: 5, blockID: 23).blockOffset) + XCTAssertEqual(23, Index(blockOffset: 5, blockID: 23).blockID) + + XCTAssertEqual( + UInt(bitPattern: Int(-105)) & Deque.maxBlockID, + Index(blockOffset: 3096, blockID: UInt(bitPattern: Int(-105))).blockID) + } + + static var allTests = [ + ("testIndexComputedProperties", testIndexComputedProperties), + ] +} diff --git a/Tests/PenguinStructuresTests/DequeTests.swift b/Tests/PenguinStructuresTests/DequeTests.swift index 730c45c8..029810ea 100644 --- a/Tests/PenguinStructuresTests/DequeTests.swift +++ b/Tests/PenguinStructuresTests/DequeTests.swift @@ -59,7 +59,6 @@ final class DequeTests: XCTestCase { for i in 2048..<(2048 * 8) { d.pushBack(i) } - XCTAssertEqual(2048 * 8, d.count) XCTAssertEqual(2048, t1.count) XCTAssertEqual(0, t0.count) @@ -73,17 +72,14 @@ final class DequeTests: XCTestCase { } } - func testHierarchicalCollection() { - var d = Deque() - for i in 0..<2048 { - d.pushBack(i) - } - XCTAssertEqual(Array(0..<2048), d.flatten()) + func testCollectionSemantics() { + let d = Deque(0..<100) + d.checkBidirectionalCollectionSemantics(expecting: 0..<100) } static var allTests = [ ("testSimple", testSimple), ("testValueSemantics", testValueSemantics), - ("testHierarchicalCollection", testHierarchicalCollection), + ("testCollectionSemantics", testCollectionSemantics), ] } diff --git a/Tests/PenguinStructuresTests/XCTestManifests.swift b/Tests/PenguinStructuresTests/XCTestManifests.swift index 3527f7c7..fd636463 100644 --- a/Tests/PenguinStructuresTests/XCTestManifests.swift +++ b/Tests/PenguinStructuresTests/XCTestManifests.swift @@ -26,6 +26,7 @@ import XCTest testCase(CollectionAlgorithmTests.allTests), testCase(ConcatenationTests.allTests), testCase(DequeTests.allTests), + testCase(DequeInternalTests.allTests), testCase(DoubleEndedBufferTests.allTests), testCase(EitherCollectionTests.allTests), testCase(EitherTests.allTests),