Skip to content

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
304 changes: 304 additions & 0 deletions stdlib/public/core/RingBuffer.swift
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
}
}
40 changes: 9 additions & 31 deletions stdlib/public/core/Sequence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor

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?

Copy link
Contributor Author

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 :)

Copy link
Contributor

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 ;-)

ringBuffer.append(contentsOf: self)
return AnySequence(ringBuffer)
}

Expand Down Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use popFirst here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ringBuffer.append(element) later will be equivalent to popFirst().

I expect that popFirst is less performant on the current ring buffer implementation. It could use a startIndex and an endIndex to change that.

Copy link
Contributor Author

@therealbnut therealbnut Feb 5, 2017

Choose a reason for hiding this comment

The 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 _ContiguousArrayBuffer is probably that, but I haven't looked into it enough yet.

Copy link
Contributor

Choose a reason for hiding this comment

The 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)
}
Expand Down
Loading