Simplify Heap implementation. (#960)

Motivation:

Our original Heap implementation used a bunch of static functions
in order to make it clear how it related to textbook heap
implementations. This was primarily done to ensure that it was easier
to see the correctness of the code.

However, we've long been satisfied with the correctness of the code,
and the Heap is one of the best tested parts of NIO. For this reason,
we should be free to refactor it.

Given https://bugs.swift.org/browse/SR-10346, we've been given an
incentive to do that refactor right now. This is because the Heap
is causing repeated CoW operations each time we enqueue new work
onto the event loop. That's a substantial performance cost: well
worth fixing with a small rewrite.

Modifications:

- Removed all static funcs from Heap
- Rewrote some into instance funcs
- Merged the implementation of insert into append.
- Added an allocation test to avoid regressing this change.

Result:

Faster Heap, happier users, sadder textbook authors.
This commit is contained in:
Cory Benfield 2019-04-10 13:53:07 +01:00 committed by GitHub
parent 56a2bee434
commit 0179535b99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 56 additions and 49 deletions

View File

@ -58,7 +58,7 @@ cd ..
"$swift_bin" run -c release | tee "$tmp/output" "$swift_bin" run -c release | tee "$tmp/output"
) )
for test in 1000_reqs_1_conn 1_reqs_1000_conn ping_pong_1000_reqs_1_conn bytebuffer_lots_of_rw future_lots_of_callbacks; do for test in 1000_reqs_1_conn 1_reqs_1000_conn ping_pong_1000_reqs_1_conn bytebuffer_lots_of_rw future_lots_of_callbacks scheduling_10000_executions; do
cat "$tmp/output" # helps debugging cat "$tmp/output" # helps debugging
total_allocations=$(grep "^$test.total_allocations:" "$tmp/output" | cut -d: -f2 | sed 's/ //g') total_allocations=$(grep "^$test.total_allocations:" "$tmp/output" | cut -d: -f2 | sed 's/ //g')
not_freed_allocations=$(grep "^$test.remaining_allocations:" "$tmp/output" | cut -d: -f2 | sed 's/ //g') not_freed_allocations=$(grep "^$test.remaining_allocations:" "$tmp/output" | cut -d: -f2 | sed 's/ //g')

View File

@ -18,7 +18,7 @@ import NIO
import NIOHTTP1 import NIOHTTP1
import Foundation import Foundation
import AtomicCounter import AtomicCounter
import Dispatch // needed for Swift 4.0 on Linux only import Dispatch
private final class SimpleHTTPServer: ChannelInboundHandler { private final class SimpleHTTPServer: ChannelInboundHandler {
typealias InboundIn = HTTPServerRequestPart typealias InboundIn = HTTPServerRequestPart
@ -436,5 +436,21 @@ public func swiftMain() -> Int {
return 1000 return 1000
} }
measureAndPrint(desc: "scheduling_10000_executions") {
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let loop = group.next()
let dg = DispatchGroup()
try! loop.submit {
for _ in 0..<10_000 {
dg.enter()
loop.execute { dg.leave() }
}
}.wait()
dg.wait()
try! group.syncShutdownGracefully()
return 10_000
}
return 0 return 0
} }

View File

@ -51,76 +51,64 @@ internal struct Heap<T: Comparable> {
self.type = type self.type = type
} }
// MARK: verbatim from CLRS's (Introduction to Algorithms) Heapsort chapter with the following changes
// - added a `compare` parameter to make it either a min or a max heap
// - renamed `largest` to `root`
// - removed `max` from the method names like `maxHeapify`, `maxHeapInsert` etc
// - made the arrays 0-based
// named `PARENT` in CLRS // named `PARENT` in CLRS
private static func parentIndex(_ i: Int) -> Int { private func parentIndex(_ i: Int) -> Int {
return (i-1) / 2 return (i-1) / 2
} }
// named `LEFT` in CLRS // named `LEFT` in CLRS
private static func leftIndex(_ i: Int) -> Int { private func leftIndex(_ i: Int) -> Int {
return 2*i + 1 return 2*i + 1
} }
// named `RIGHT` in CLRS // named `RIGHT` in CLRS
private static func rightIndex(_ i: Int) -> Int { private func rightIndex(_ i: Int) -> Int {
return 2*i + 2 return 2*i + 2
} }
// named `MAX-HEAPIFY` in CLRS // named `MAX-HEAPIFY` in CLRS
private static func heapify(storage: inout ContiguousArray<T>, compare: (T, T) -> Bool, _ i: Int) { private mutating func heapify(_ index: Int) {
let l = Heap<T>.leftIndex(i) let left = self.leftIndex(index)
let r = Heap<T>.rightIndex(i) let right = self.rightIndex(index)
var root: Int var root: Int
if l <= (storage.count - 1) && compare(storage[l], storage[i]) { if left <= (self.storage.count - 1) && self.comparator(storage[left], storage[index]) {
root = l root = left
} else { } else {
root = i root = index
} }
if r <= (storage.count - 1) && compare(storage[r], storage[root]) { if right <= (self.storage.count - 1) && self.comparator(storage[right], storage[root]) {
root = r root = right
} }
if root != i { if root != index {
storage.swapAt(i, root) self.storage.swapAt(index, root)
heapify(storage: &storage, compare: compare, root) self.heapify(root)
}
}
// named `MAX-HEAP-INSERT` in CRLS
private static func heapInsert(storage: inout ContiguousArray<T>, compare: (T, T) -> Bool, key: T) {
var i = storage.count
storage.append(key)
while i > 0 && compare(storage[i], storage[parentIndex(i)]) {
storage.swapAt(i, parentIndex(i))
i = parentIndex(i)
} }
} }
// named `HEAP-INCREASE-KEY` in CRLS // named `HEAP-INCREASE-KEY` in CRLS
private static func heapRootify(storage: inout ContiguousArray<T>, compare: (T, T) -> Bool, index: Int, key: T) { private mutating func heapRootify(index: Int, key: T) {
var index = index var index = index
if compare(storage[index], key) { if self.comparator(storage[index], key) {
fatalError("New key must be closer to the root than current key") fatalError("New key must be closer to the root than current key")
} }
storage[index] = key self.storage[index] = key
while index > 0 && compare(storage[index], storage[parentIndex(index)]) { while index > 0 && self.comparator(self.storage[index], self.storage[self.parentIndex(index)]) {
storage.swapAt(index, parentIndex(index)) self.storage.swapAt(index, self.parentIndex(index))
index = parentIndex(index) index = self.parentIndex(index)
} }
} }
// MARK: Swift interface using the low-level methods above
public mutating func append(_ value: T) { public mutating func append(_ value: T) {
Heap<T>.heapInsert(storage: &self.storage, compare: self.comparator, key: value) var i = self.storage.count
self.storage.append(value)
while i > 0 && self.comparator(self.storage[i], self.storage[self.parentIndex(i)]) {
self.storage.swapAt(i, self.parentIndex(i))
i = self.parentIndex(i)
}
} }
@discardableResult @discardableResult
@ -144,23 +132,24 @@ internal struct Heap<T: Comparable> {
return nil return nil
} }
let element = self.storage[index] let element = self.storage[index]
let comparator = self.comparator
if self.storage.count == 1 || self.storage[index] == self.storage[self.storage.count - 1] { if self.storage.count == 1 || self.storage[index] == self.storage[self.storage.count - 1] {
self.storage.removeLast() self.storage.removeLast()
} else if !self.comparator(self.storage[index], self.storage[self.storage.count - 1]) { } else if !comparator(self.storage[index], self.storage[self.storage.count - 1]) {
Heap<T>.heapRootify(storage: &self.storage, compare: self.comparator, index: index, key: self.storage[self.storage.count - 1]) self.heapRootify(index: index, key: self.storage[self.storage.count - 1])
self.storage.removeLast() self.storage.removeLast()
} else { } else {
self.storage[index] = self.storage[self.storage.count - 1] self.storage[index] = self.storage[self.storage.count - 1]
self.storage.removeLast() self.storage.removeLast()
Heap<T>.heapify(storage: &self.storage, compare: self.comparator, index) self.heapify(index)
} }
return element return element
} }
internal func checkHeapProperty() -> Bool { internal func checkHeapProperty() -> Bool {
func checkHeapProperty(index: Int) -> Bool { func checkHeapProperty(index: Int) -> Bool {
let li = Heap<T>.leftIndex(index) let li = self.leftIndex(index)
let ri = Heap<T>.rightIndex(index) let ri = self.rightIndex(index)
if index >= self.storage.count { if index >= self.storage.count {
return true return true
} else { } else {
@ -204,8 +193,8 @@ extension Heap: CustomDebugStringConvertible {
var all = "\n" var all = "\n"
let spacing = String(repeating: " ", count: maxLen) let spacing = String(repeating: " ", count: maxLen)
func subtreeWidths(rootIndex: Int) -> (Int, Int) { func subtreeWidths(rootIndex: Int) -> (Int, Int) {
let lcIdx = Heap<T>.leftIndex(rootIndex) let lcIdx = self.leftIndex(rootIndex)
let rcIdx = Heap<T>.rightIndex(rootIndex) let rcIdx = self.rightIndex(rootIndex)
var leftSpace = 0 var leftSpace = 0
var rightSpace = 0 var rightSpace = 0
if lcIdx < self.storage.count { if lcIdx < self.storage.count {

View File

@ -18,20 +18,22 @@ services:
image: swift-nio:18.04-5.0 image: swift-nio:18.04-5.0
environment: environment:
- MAX_ALLOCS_ALLOWED_1000_reqs_1_conn=31200 - MAX_ALLOCS_ALLOWED_1000_reqs_1_conn=31200
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=1177050 # was: 685050 - MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=1155050 # was: 685050
- MAX_ALLOCS_ALLOWED_ping_pong_1000_reqs_1_conn=4600 - MAX_ALLOCS_ALLOWED_ping_pong_1000_reqs_1_conn=4600
- MAX_ALLOCS_ALLOWED_bytebuffer_lots_of_rw=2100 - MAX_ALLOCS_ALLOWED_bytebuffer_lots_of_rw=2100
- MAX_ALLOCS_ALLOWED_future_lots_of_callbacks=99100 - MAX_ALLOCS_ALLOWED_future_lots_of_callbacks=99100
- MAX_ALLOCS_ALLOWED_scheduling_10000_executions=20150
test: test:
image: swift-nio:18.04-5.0 image: swift-nio:18.04-5.0
command: /bin/bash -cl "swift test -Xswiftc -warnings-as-errors && ./scripts/integration_tests.sh" command: /bin/bash -cl "swift test -Xswiftc -warnings-as-errors && ./scripts/integration_tests.sh"
environment: environment:
- MAX_ALLOCS_ALLOWED_1000_reqs_1_conn=31200 - MAX_ALLOCS_ALLOWED_1000_reqs_1_conn=31200
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=1177050 # was: 685050 - MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=1155050 # was: 685050
- MAX_ALLOCS_ALLOWED_ping_pong_1000_reqs_1_conn=4600 - MAX_ALLOCS_ALLOWED_ping_pong_1000_reqs_1_conn=4600
- MAX_ALLOCS_ALLOWED_bytebuffer_lots_of_rw=2100 - MAX_ALLOCS_ALLOWED_bytebuffer_lots_of_rw=2100
- MAX_ALLOCS_ALLOWED_future_lots_of_callbacks=99100 - MAX_ALLOCS_ALLOWED_future_lots_of_callbacks=99100
- MAX_ALLOCS_ALLOWED_scheduling_10000_executions=20150
echo: echo:
image: swift-nio:18.04-5.0 image: swift-nio:18.04-5.0