-
Notifications
You must be signed in to change notification settings - Fork 10.5k
WIP: RingBuffer #7250
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
WIP: RingBuffer #7250
Changes from all commits
bf5df36
084944f
67b54e9
73fead0
f34ad15
0147774
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,304 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Swift.org open source project | ||
// | ||
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors | ||
// Licensed under Apache License v2.0 with Runtime Library Exception | ||
// | ||
// See https://swift.org/LICENSE.txt for license information | ||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
internal struct RingBuffer<Element>: | ||
RangeReplaceableCollection, MutableCollection, RandomAccessCollection, | ||
MutableCollectionAlgorithms | ||
{ | ||
// Stores up to _bufferCapacity elements into _buffer the indices start at | ||
// _indexOffset and wrap around. | ||
// | ||
// The notation [0,1][2,3,4] indicates an internal state of: | ||
// _buffer: [2,3,4,0,1] | ||
// _indexOffset: 3 ^ | ||
// _bufferCapacity: 5 | ||
// | ||
// If _buffer.count < _bufferCapacity then this has a few implications: | ||
// * the buffer is not full | ||
// * new elements will be appended to the end of the buffer (for speed) | ||
// * _indexOffset must be zero (0) | ||
// | ||
// Algorithms used in this implementation aim to be O(1) in additional | ||
// memory usage, even at the expense of performance. | ||
|
||
private var _bufferCapacity: Int | ||
fileprivate var _buffer: ContiguousArray<Element> | ||
fileprivate var _indexOffset: Int | ||
|
||
public typealias Indices = CountableRange<Int> | ||
public typealias Iterator = IndexingIterator<RingBuffer> | ||
public typealias SubSequence = ArraySlice<Element> | ||
|
||
private func _checkIndex(_ position: Int) { | ||
_precondition(position <= _buffer.count + _bufferCapacity, | ||
"RingBuffer index is out of range") | ||
} | ||
|
||
public init() { | ||
self.init(capacity: 1) | ||
} | ||
|
||
public init(capacity: Int) { | ||
var buffer = ContiguousArray<Element>() | ||
buffer.reserveCapacity(capacity) | ||
self.init(buffer, capacity: capacity, offset: 0) | ||
} | ||
init(_ buffer: ContiguousArray<Element>, capacity: Int, offset: Int) { | ||
_bufferCapacity = capacity | ||
_buffer = buffer | ||
_indexOffset = offset | ||
} | ||
|
||
public var startIndex: Int { | ||
return 0 | ||
} | ||
public var endIndex: Int { | ||
return _buffer.count | ||
} | ||
public var underestimatedCount: Int { | ||
return _buffer.count | ||
} | ||
public var isFull: Bool { | ||
return _buffer.count == _bufferCapacity | ||
} | ||
public var count: Int { | ||
return _buffer.count | ||
} | ||
public var capacity: Int { | ||
return _bufferCapacity | ||
} | ||
|
||
public mutating func reserveCapacity(_ n: Int) { | ||
rotate(shiftingToStart: _indexOffset) | ||
_bufferCapacity = Swift.max(n, _buffer.count) | ||
_buffer.reserveCapacity(_bufferCapacity) | ||
} | ||
|
||
public subscript(bounds: Range<Int>) -> SubSequence { | ||
get { | ||
let count = _buffer.count | ||
_precondition(bounds.count <= count) | ||
_checkIndex(bounds.lowerBound) | ||
_checkIndex(bounds.upperBound) | ||
let lowerBound = _indexOffset + bounds.lowerBound | ||
let upperBound = _indexOffset + bounds.upperBound | ||
guard lowerBound < count else { | ||
return _buffer[(lowerBound - count) ..< (upperBound - count)] | ||
} | ||
guard upperBound > count else { | ||
return _buffer[lowerBound ..< upperBound] | ||
} | ||
let lhs = _buffer[lowerBound ..< count] | ||
let rhs = _buffer[0 ..< (upperBound - count)] | ||
return SubSequence([lhs, rhs].joined()) | ||
} | ||
set { | ||
replaceSubrange(bounds, with: newValue) | ||
} | ||
} | ||
public subscript(position: Int) -> Element { | ||
get { | ||
_checkIndex(position) | ||
let index = (_indexOffset + position) % _buffer.count | ||
return _buffer[index] | ||
} | ||
set { | ||
_checkIndex(position) | ||
let index = (_indexOffset + position) % _buffer.count | ||
_buffer[index] = newValue | ||
} | ||
} | ||
|
||
public func index(after i: Int) -> Int { | ||
return i + 1 | ||
} | ||
public func index(before i: Int) -> Int { | ||
return i - 1 | ||
} | ||
|
||
public mutating func replaceSubrange<C>( | ||
_ subrange: Range<Index>, with newElements: C) where | ||
C : Collection, Element == C.Iterator.Element | ||
{ | ||
guard !newElements.isEmpty else { | ||
removeSubrange(subrange) | ||
return | ||
} | ||
guard !isEmpty else { | ||
_precondition(subrange.lowerBound == 0) | ||
append(contentsOf: newElements) | ||
return | ||
} | ||
|
||
let count = _buffer.count | ||
|
||
// FIXME: Is there a better way to do this | ||
// it's potentially O(n) in newElements, and has an unsafe cast | ||
let newCount = Int(newElements.count.toIntMax()) | ||
// the change in self.count after inserting the elements | ||
let offsMin = -subrange.count, offsMax = _bufferCapacity - count | ||
let offset = Swift.max(offsMin, Swift.min(newCount-subrange.count, offsMax)) | ||
var suffix = newCount.makeIterator() | ||
|
||
// equivalent of suffix(suffixCount), which can't be used as it uses a | ||
// ring buffer, this is also O(1) in memory. | ||
let suffixCount = Swift.max(0, offset) + subrange.count | ||
for _ in 0 ..< Swift.max(0, elementsCount-suffixCount) { | ||
_ = suffix.next() | ||
} | ||
|
||
// If the total number of elements doesn't increase only elements in | ||
// subrange need to be modified. | ||
// We can replace the elements of subrange, then remove excess subrange | ||
// elements. | ||
if offset <= 0 { | ||
if !subrange.isEmpty { | ||
var index = subrange.lowerBound | ||
while let element = suffix.next() { | ||
self[index] = element | ||
index += 1 | ||
// FIXME: This should never happen, due to suffix above | ||
if index == subrange.upperBound { | ||
index = subrange.lowerBound | ||
} | ||
} | ||
} | ||
removeSubrange((subrange.upperBound+offset) ..< subrange.upperBound) | ||
} | ||
// If the total number of elements increases: | ||
// 1. move elements to the end as needed, or until the buffer is big enough | ||
// 2. insert the new elements into subrange and the new buffer | ||
else { | ||
_precondition(count < _bufferCapacity) | ||
_precondition(_indexOffset == 0) | ||
// FIXME: No need for `uninitializedElement` if elements can be | ||
// uninitialized in a lower-level container. | ||
// Copy elements to the end, until there's room for newElements | ||
let uninitializedElement = _buffer[0] | ||
for index in (count-offset) ..< count { | ||
_buffer.append(index < 0 ? uninitializedElement : _buffer[index]) | ||
} | ||
let range = subrange.lowerBound ..< (subrange.upperBound + offset) | ||
var index = range.lowerBound | ||
while let element = suffix.next() { | ||
_buffer[index] = element | ||
index += 1 | ||
// FIXME: This should never happen, due to suffix above | ||
if index == range.upperBound { | ||
index = range.lowerBound | ||
} | ||
} | ||
} | ||
} | ||
|
||
public mutating func removeSubrange(_ bounds: Range<Int>) { | ||
let count = _buffer.count | ||
_precondition(bounds.count <= count) | ||
_checkIndex(bounds.lowerBound) | ||
_checkIndex(bounds.upperBound) | ||
guard bounds.lowerBound < bounds.upperBound else { | ||
return | ||
} | ||
guard bounds.count < count else { | ||
removeAll() | ||
return | ||
} | ||
|
||
let newCount = count - bounds.count | ||
|
||
var lowerBound = _indexOffset + bounds.lowerBound | ||
var upperBound = _indexOffset + bounds.upperBound | ||
if lowerBound >= _bufferCapacity { | ||
lowerBound -= _bufferCapacity | ||
upperBound -= _bufferCapacity | ||
} | ||
|
||
if _indexOffset == 0 { | ||
for i in 0 ..< (newCount - bounds.lowerBound) { | ||
_buffer[bounds.lowerBound + i] = _buffer[bounds.upperBound + i] | ||
} | ||
} | ||
else { | ||
for i in bounds.lowerBound ..< newCount { | ||
let from = (_indexOffset + i + bounds.count) % _bufferCapacity | ||
let to = (_indexOffset + i) % _bufferCapacity | ||
_buffer[to] = _buffer[from] | ||
} | ||
rotate(shiftingToStart: _indexOffset) | ||
} | ||
|
||
_buffer.removeSubrange(newCount ..< count) | ||
_indexOffset = 0 | ||
} | ||
|
||
public mutating func removeAll() { | ||
_indexOffset = 0 | ||
_buffer.removeAll(keepingCapacity: true) | ||
} | ||
|
||
public mutating func append(_ newElement: Element) { | ||
if _buffer.count < _bufferCapacity { | ||
_buffer.append(newElement) | ||
} | ||
else { | ||
_buffer[_indexOffset] = newElement | ||
_indexOffset = (_indexOffset + 1) % _bufferCapacity | ||
} | ||
} | ||
} | ||
|
||
extension RingBuffer: ExpressibleByArrayLiteral { | ||
public init(arrayLiteral elements: Element...) { | ||
let buffer = ContiguousArray(elements) | ||
self.init(buffer, capacity: buffer.count, offset: 0) | ||
} | ||
} | ||
|
||
extension RingBuffer: CustomStringConvertible, CustomDebugStringConvertible { | ||
public var description: String { | ||
var output = "[" | ||
if !isEmpty { | ||
output.reserveCapacity(2 + count * 3) | ||
output.append(self.lazy | ||
.map(String.init(describing:)) | ||
.joined(separator: ", ")) | ||
} | ||
output.append("]") | ||
return output | ||
} | ||
|
||
public var debugDescription: String { | ||
var output = "RingBuffer<" | ||
output.append(String(describing: Element.self)) | ||
output.append(",\(capacity)>([") | ||
if !_buffer.isEmpty { | ||
output.reserveCapacity(2 + count * 3) | ||
if _buffer.count < _bufferCapacity { | ||
output.append(self.lazy | ||
.map(String.init(describing:)) | ||
.joined(separator: ", ")) | ||
} | ||
else { | ||
output.append(_buffer[_indexOffset ..< _buffer.count].lazy | ||
.map(String.init(describing:)) | ||
.joined(separator: ", ")) | ||
output.append("][") | ||
output.append(_buffer[0 ..< _indexOffset].lazy | ||
.map(String.init(describing:)) | ||
.joined(separator: ", ")) | ||
} | ||
} | ||
output.append("])") | ||
|
||
return output | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -879,31 +879,14 @@ extension Sequence { | |
public func suffix(_ maxLength: Int) -> AnySequence<Iterator.Element> { | ||
_precondition(maxLength >= 0, "Can't take a suffix of negative length from a sequence") | ||
if maxLength == 0 { return AnySequence([]) } | ||
// FIXME: <rdar://problem/21885650> Create reusable RingBuffer<T> | ||
|
||
// Put incoming elements into a ring buffer to save space. Once all | ||
// elements are consumed, reorder the ring buffer into an `Array` | ||
// and return it. This saves memory for sequences particularly longer | ||
// than `maxLength`. | ||
var ringBuffer: [Iterator.Element] = [] | ||
ringBuffer.reserveCapacity(Swift.min(maxLength, underestimatedCount)) | ||
|
||
var i = ringBuffer.startIndex | ||
|
||
for element in self { | ||
if ringBuffer.count < maxLength { | ||
ringBuffer.append(element) | ||
} else { | ||
ringBuffer[i] = element | ||
i += 1 | ||
i %= maxLength | ||
} | ||
} | ||
|
||
if i != ringBuffer.startIndex { | ||
let s0 = ringBuffer[i..<ringBuffer.endIndex] | ||
let s1 = ringBuffer[0..<i] | ||
return AnySequence([s0, s1].joined()) | ||
} | ||
let capacity = Swift.min(maxLength, underestimatedCount) | ||
var ringBuffer = RingBuffer<Iterator.Element>(capacity: capacity) | ||
ringBuffer.append(contentsOf: self) | ||
return AnySequence(ringBuffer) | ||
} | ||
|
||
|
@@ -1206,24 +1189,19 @@ extension Sequence where | |
_precondition(n >= 0, "Can't drop a negative number of elements from a sequence") | ||
if n == 0 { return AnySequence(self) } | ||
|
||
// FIXME: <rdar://problem/21885650> Create reusable RingBuffer<T> | ||
// Put incoming elements from this sequence in a holding tank, a ring buffer | ||
// of size <= n. If more elements keep coming in, pull them out of the | ||
// holding tank into the result, an `Array`. This saves | ||
// `n` * sizeof(Iterator.Element) of memory, because slices keep the entire | ||
// memory of an `Array` alive. | ||
var result: [Iterator.Element] = [] | ||
var ringBuffer: [Iterator.Element] = [] | ||
var i = ringBuffer.startIndex | ||
|
||
var ringBuffer = RingBuffer<Iterator.Element>(capacity: n) | ||
|
||
for element in self { | ||
if ringBuffer.count < n { | ||
ringBuffer.append(element) | ||
} else { | ||
result.append(ringBuffer[i]) | ||
ringBuffer[i] = element | ||
i = ringBuffer.index(after: i) % n | ||
if ringBuffer.isFull, let first = ringBuffer.first { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use popFirst here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The I expect that popFirst is less performant on the current ring buffer implementation. It could use a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was going to implement it like that, but I needed to investigate existing array-like structures more first. I'd like a structure that's not reallocated, and can have its elements initialised and de-initialized out-of-order as needed. I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You want ManagedBuffer. Cheers! |
||
result.append(first) | ||
} | ||
ringBuffer.append(element) | ||
} | ||
return AnySequence(result) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems to me this can't possibly build, since the standard library doesn't define RingBuffer anywhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was intending that it's
internal
in the stdlib, for now - I've only just moved it over from a separate project so I haven't worked out all the details of integrating it into the stdlib yet. I'm not going to do that until I know it's a welcome contribution though :)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the PR is just meant to fix the FIXME, it's definitely welcome! However, you'll have to get it integrated so we can run the tests and benchmarks. And if there's no benchmark that will be affected by your improvement, I'll ask you to please write one ;-)