Skip to content

Commit

Permalink
Use power of two for underlying storage of CircularBuffer (#313)
Browse files Browse the repository at this point in the history
Motivation:

We should use power of two for the underlying storage to allow us to make use of bitmasking which is faster then modulo operations.

Modifications:

Use power of two and replace module with bitmasking.

Result:

Faster implementation of CircularBuffer.
  • Loading branch information
normanmaurer authored and weissi committed Apr 23, 2018
1 parent 7fe7ed6 commit cbb3f19
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 45 deletions.
80 changes: 38 additions & 42 deletions Sources/NIO/CircularBuffer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,62 +26,61 @@ public protocol AppendableCollection: Collection {
public struct CircularBuffer<E>: CustomStringConvertible, AppendableCollection {
private var buffer: ContiguousArray<E?>

/// The capacity of the underlying buffer
private var bufferCapacity: Int

/// The index into the buffer of the first item
private var startIdx = 0
private(set) /* private but tests */ internal var headIdx = 0

/// The index into the buffer of the next free slot
private var endIdx = 0
private(set) /* private but tests */ internal var tailIdx = 0

/// The number of items in the ring part of this buffer
private var ringLength = 0
/// Bitmask used for calculating the tailIdx / headIdx based on the fact that the underlying storage
/// has always a size of power of two.
private var mask: Int {
return self.buffer.count - 1
}

/// Allocates a buffer that can hold up to `initialRingCapacity` elements and initialise an empty ring backed by
/// the buffer. When the ring grows to more than `initialRingCapacity` elements the buffer will be expanded.
public init(initialRingCapacity: Int) {
self.bufferCapacity = initialRingCapacity
self.buffer = ContiguousArray<E?>(repeating: nil, count: Int(initialRingCapacity))
let capacity = Int(UInt32(initialRingCapacity).nextPowerOf2())
self.buffer = ContiguousArray<E?>(repeating: nil, count: capacity)
assert(self.buffer.count == capacity)
}

/// Append an element to the end of the ring buffer.
///
/// Amortized *O(1)*
public mutating func append(_ value: E) {
let expandBuf: Bool = self.bufferCapacity == self.ringLength
self.buffer[self.tailIdx] = value
self.tailIdx = (self.tailIdx + 1) & self.mask

if expandBuf {
if self.headIdx == self.tailIdx {
// No more room left for another append so grow the buffer now.
var newBacking: ContiguousArray<E?> = []
let newCapacity = Swift.max(1, 2 * self.bufferCapacity)
let newCapacity = self.buffer.count << 1 // Double the storage.
precondition(newCapacity > 0, "Can't double capacity of \(self.buffer.count)")
assert(newCapacity % 2 == 0)

newBacking.reserveCapacity(newCapacity)
newBacking.append(contentsOf: self.buffer[self.startIdx..<self.bufferCapacity])
if startIdx > 0 {
newBacking.append(contentsOf: self.buffer[0..<self.startIdx])
newBacking.append(contentsOf: self.buffer[self.headIdx..<self.buffer.count])
if self.headIdx > 0 {
newBacking.append(contentsOf: self.buffer[0..<self.headIdx])
}
newBacking.append(contentsOf: repeatElement(nil, count: newCapacity - newBacking.count))
self.tailIdx = self.buffer.count
self.headIdx = 0
self.buffer = newBacking
self.startIdx = 0
self.endIdx = self.ringLength
self.bufferCapacity = newCapacity
precondition(self.bufferCapacity == self.buffer.count)
}

self.buffer[self.endIdx] = value
self.ringLength += 1
self.endIdx = (self.endIdx + 1) % self.bufferCapacity
}

/// Remove the front element of the ring buffer.
///
/// *O(1)*
public mutating func removeFirst() -> E {
precondition(self.ringLength != 0)
precondition(!self.isEmpty)

let value = self.buffer[self.startIdx]
self.buffer[startIdx] = nil
self.ringLength -= 1
self.startIdx = (self.startIdx + 1) % self.bufferCapacity
let value = self.buffer[self.headIdx]
self.buffer[headIdx] = nil
self.headIdx = (self.headIdx + 1) & self.mask

return value!
}
Expand All @@ -93,16 +92,13 @@ public struct CircularBuffer<E>: CustomStringConvertible, AppendableCollection {
if self.isEmpty {
return nil
} else {
return self.buffer[self.startIdx]
return self.buffer[self.headIdx]
}
}

private func bufferIndex(ofIndex index: Int) -> Int {
if index < self.ringLength {
return (self.startIdx + index) % self.bufferCapacity
} else {
fatalError("index out of range")
}
precondition(index < self.count, "index out of range")
return (self.headIdx + index) & self.mask
}

// MARK: Collection implementation
Expand All @@ -129,17 +125,17 @@ public struct CircularBuffer<E>: CustomStringConvertible, AppendableCollection {

/// Return all valid indices of the ring.
public var indices: CountableRange<Int> {
return 0..<self.ringLength
return 0..<self.count
}

/// Returns whether the ring is empty.
public var isEmpty: Bool {
return self.ringLength == 0
return self.headIdx == self.tailIdx
}

/// Returns the number of element in the ring.
public var count: Int {
return self.ringLength
return (self.tailIdx - self.headIdx) & self.mask
}

/// Returns the index of the first element of the ring.
Expand All @@ -149,13 +145,13 @@ public struct CircularBuffer<E>: CustomStringConvertible, AppendableCollection {

/// Returns the ring's "past the end" position -- that is, the position one greater than the last valid subscript argument.
public var endIndex: Int {
return self.ringLength
return self.count
}

/// Returns the next index after `index`.
public func index(after: Int) -> Int {
let nextIndex = after + 1
precondition(nextIndex <= endIndex)
precondition(nextIndex <= self.endIndex)
return nextIndex
}

Expand All @@ -164,15 +160,15 @@ public struct CircularBuffer<E>: CustomStringConvertible, AppendableCollection {
public var description: String {
var desc = "[ "
for el in self.buffer.enumerated() {
if el.0 == self.startIdx {
if el.0 == self.headIdx {
desc += "<"
} else if el.0 == self.endIdx {
} else if el.0 == self.tailIdx {
desc += ">"
}
desc += el.1.map { "\($0) " } ?? "_ "
}
desc += "]"
desc += " (bufferCapacity: \(self.bufferCapacity), ringLength: \(self.ringLength))"
desc += " (bufferCapacity: \(self.buffer.count), ringLength: \(self.count))"
return desc
}
}
2 changes: 2 additions & 0 deletions Tests/NIOTests/CircularBufferTests+XCTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ extension CircularBufferTests {
("testEmptyingExpandedRingWorks", testEmptyingExpandedRingWorks),
("testChangeElements", testChangeElements),
("testSliceTheRing", testSliceTheRing),
("testCount", testCount),
("testFirst", testFirst),
]
}
}
Expand Down
55 changes: 52 additions & 3 deletions Tests/NIOTests/CircularBufferTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//===----------------------------------------------------------------------===//

import XCTest
import NIO
@testable import NIO

class CircularBufferTests: XCTestCase {
func testTrivial() {
Expand All @@ -24,20 +24,31 @@ class CircularBufferTests: XCTestCase {

func testAddRemoveInALoop() {
var ring = CircularBuffer<Int>(initialRingCapacity: 8)
XCTAssertTrue(ring.isEmpty)
XCTAssertEqual(0, ring.count)

for f in 0..<1000 {
ring.append(f)
XCTAssertEqual(f, ring.removeFirst())
XCTAssertTrue(ring.isEmpty)
XCTAssertEqual(0, ring.count)
}
}

func testAddAllRemoveAll() {
var ring = CircularBuffer<Int>(initialRingCapacity: 8)
for f in 0..<1000 {
XCTAssertTrue(ring.isEmpty)
XCTAssertEqual(0, ring.count)

for f in 1..<1000 {
ring.append(f)
XCTAssertEqual(f, ring.count)
}
for f in 0..<1000 {
for f in 1..<1000 {
XCTAssertEqual(f, ring.removeFirst())
XCTAssertEqual(999 - f, ring.count)
}
XCTAssertTrue(ring.isEmpty)
}

func testHarderExpansion() {
Expand Down Expand Up @@ -106,6 +117,9 @@ class CircularBufferTests: XCTestCase {
ring.append(idx)
}

XCTAssertFalse(ring.isEmpty)
XCTAssertEqual(5, ring.count)

XCTAssertEqual(ring.indices, 0..<5)
XCTAssertEqual(ring.startIndex, 0)
XCTAssertEqual(ring.endIndex, 5)
Expand All @@ -123,6 +137,7 @@ class CircularBufferTests: XCTestCase {
for idx in 0..<4 {
ring.append(idx)
}
XCTAssertEqual(4, ring.count)
XCTAssertFalse(ring.isEmpty)
}

Expand Down Expand Up @@ -207,4 +222,38 @@ class CircularBufferTests: XCTestCase {
XCTAssertEqual(idx + 25, element)
}
}

func testCount() {
var ring = CircularBuffer<Int>(initialRingCapacity: 4)
ring.append(1)
XCTAssertEqual(1, ring.count)
ring.append(2)
XCTAssertEqual(2, ring.count)
XCTAssertEqual(1, ring.removeFirst())
ring.append(3)
XCTAssertEqual(2, ring.count)
XCTAssertEqual(2, ring.removeFirst())
ring.append(4)

XCTAssertEqual(2, ring.count)
XCTAssertEqual(3, ring.removeFirst())
ring.append(5)

XCTAssertEqual(3, ring.headIdx)
XCTAssertEqual(1, ring.tailIdx)
XCTAssertEqual(2, ring.count)
XCTAssertEqual(4, ring.removeFirst())
XCTAssertEqual(5, ring.removeFirst())
XCTAssertEqual(0, ring.count)
XCTAssertTrue(ring.isEmpty)
}

func testFirst() {
var ring = CircularBuffer<Int>(initialRingCapacity: 3)
XCTAssertNil(ring.first)
ring.append(1)
XCTAssertEqual(1, ring.first)
XCTAssertEqual(1, ring.removeFirst())
XCTAssertNil(ring.first)
}
}

0 comments on commit cbb3f19

Please # to comment.