Specialise succeeded Void futures so we allocate less (#1703)

Motivation:

Succeeded `EventLoopFuture<Void>`s are quite important in SwiftNIO, they
happen all over the place. Unfortunately, we usually allocate each time,
unnecessarily.

Modifications:

Offer `EventLoop`s the option to cache succeeded void futures.

Result:

Fewer allocations.
This commit is contained in:
Johannes Weiss 2021-01-15 17:07:15 +00:00 committed by GitHub
parent 6b4a8197f9
commit 2b821b2ef6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 196 additions and 17 deletions

View File

@ -266,6 +266,20 @@ public protocol EventLoop: EventLoopGroup {
/// Asserts that the current thread is _not_ the one tied to this `EventLoop`.
/// Otherwise, the process will be abnormally terminated as per the semantics of `preconditionFailure(_:file:line:)`.
func preconditionNotInEventLoop(file: StaticString, line: UInt)
/// Return a succeeded `Void` future.
///
/// Semantically, this function is equivalent to calling `makeSucceededFuture(())`.
/// Contrary to `makeSucceededFuture`, `makeSucceededVoidFuture` is a customization point for `EventLoop`s which
/// allows `EventLoop`s to cache a pre-succeded `Void` future to prevent superfluous allocations.
func makeSucceededVoidFuture() -> EventLoopFuture<Void>
}
extension EventLoop {
/// Default implementation of `makeSucceededVoidFuture`: Return a fresh future (which will allocate).
public func makeSucceededVoidFuture() -> EventLoopFuture<Void> {
return EventLoopFuture(eventLoop: self, value: (), file: "n/a", line: 0)
}
}
extension EventLoopGroup {
@ -586,7 +600,12 @@ extension EventLoop {
/// - returns: a succeeded `EventLoopFuture`.
@inlinable
public func makeSucceededFuture<Success>(_ value: Success, file: StaticString = #file, line: UInt = #line) -> EventLoopFuture<Success> {
return EventLoopFuture<Success>(eventLoop: self, value: value, file: file, line: line)
if Success.self == Void.self {
// The as! will always succeed because we previously checked that Success.self == Void.self.
return self.makeSucceededVoidFuture() as! EventLoopFuture<Success>
} else {
return EventLoopFuture<Success>(eventLoop: self, value: value, file: file, line: line)
}
}
/// An `EventLoop` forms a singular `EventLoopGroup`, returning itself as the 'next' `EventLoop`.

View File

@ -59,6 +59,12 @@ internal final class SelectableEventLoop: EventLoop {
@usableFromInline
internal var _scheduledTasks = PriorityQueue<ScheduledTask>()
private var tasksCopy = ContiguousArray<() -> Void>()
@usableFromInline
internal var _succeededVoidFuture: Optional<EventLoopFuture<Void>> = nil {
didSet {
self.assertInEventLoop()
}
}
private let canBeShutdownIndividually: Bool
@usableFromInline
@ -150,6 +156,8 @@ internal final class SelectableEventLoop: EventLoop {
// We will process 4096 tasks per while loop.
self.tasksCopy.reserveCapacity(4096)
self.canBeShutdownIndividually = canBeShutdownIndividually
// note: We are creating a reference cycle here that we'll break when shutting the SelectableEventLoop down.
self._succeededVoidFuture = EventLoopFuture(eventLoop: self, value: (), file: "n/a", line: 0)
}
deinit {
@ -461,6 +469,9 @@ internal final class SelectableEventLoop: EventLoop {
// This EventLoop was closed so also close the underlying selector.
try self._selector.close()
// This breaks the retain cycle created in `init`.
self._succeededVoidFuture = nil
}
internal func initiateClose(queue: DispatchQueue, completionHandler: @escaping (Result<Void, Error>) -> Void) {
@ -560,6 +571,14 @@ internal final class SelectableEventLoop: EventLoop {
}
}
}
@inlinable
public func makeSucceededVoidFuture() -> EventLoopFuture<Void> {
guard self.inEventLoop, let voidFuture = self._succeededVoidFuture else {
return EventLoopFuture(eventLoop: self, value: (), file: "n/a", line: 0)
}
return voidFuture
}
}
extension SelectableEventLoop: CustomStringConvertible, CustomDebugStringConvertible {

View File

@ -79,6 +79,9 @@ extension EventLoopTest {
("testSchedulingTaskOnFutureFailedByELShutdownDoesNotMakeUsExplode", testSchedulingTaskOnFutureFailedByELShutdownDoesNotMakeUsExplode),
("testEventLoopGroupProvider", testEventLoopGroupProvider),
("testScheduleMaximum", testScheduleMaximum),
("testEventLoopsWithPreSucceededFuturesCacheThem", testEventLoopsWithPreSucceededFuturesCacheThem),
("testEventLoopsWithoutPreSucceededFuturesDoNotCacheThem", testEventLoopsWithoutPreSucceededFuturesDoNotCacheThem),
("testSelectableEventLoopHasPreSucceededFuturesOnlyOnTheEventLoop", testSelectableEventLoopHasPreSucceededFuturesOnlyOnTheEventLoop),
]
}
}

View File

@ -1317,4 +1317,142 @@ public final class EventLoopTest : XCTestCase {
XCTAssertEqual(error as? EventLoopError, .cancelled)
}
func testEventLoopsWithPreSucceededFuturesCacheThem() {
let el = EventLoopWithPreSucceededFuture()
defer {
XCTAssertNoThrow(try el.syncShutdownGracefully())
}
let future1 = el.makeSucceededFuture(())
let future2 = el.makeSucceededFuture(())
let future3 = el.makeSucceededVoidFuture()
XCTAssert(future1 === future2)
XCTAssert(future2 === future3)
}
func testEventLoopsWithoutPreSucceededFuturesDoNotCacheThem() {
let el = EventLoopWithoutPreSucceededFuture()
defer {
XCTAssertNoThrow(try el.syncShutdownGracefully())
}
let future1 = el.makeSucceededFuture(())
let future2 = el.makeSucceededFuture(())
let future3 = el.makeSucceededVoidFuture()
XCTAssert(future1 !== future2)
XCTAssert(future2 !== future3)
XCTAssert(future1 !== future3)
}
func testSelectableEventLoopHasPreSucceededFuturesOnlyOnTheEventLoop() {
let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
XCTAssertNoThrow(try elg.syncShutdownGracefully())
}
let el = elg.next()
let futureOutside1 = el.makeSucceededVoidFuture()
let futureOutside2 = el.makeSucceededFuture(())
XCTAssert(futureOutside1 !== futureOutside2)
XCTAssertNoThrow(try el.submit {
let futureInside1 = el.makeSucceededVoidFuture()
let futureInside2 = el.makeSucceededFuture(())
XCTAssert(futureOutside1 !== futureInside1)
XCTAssert(futureInside1 === futureInside2)
}.wait())
}
}
fileprivate class EventLoopWithPreSucceededFuture: EventLoop {
var inEventLoop: Bool {
return true
}
func execute(_ task: @escaping () -> Void) {
preconditionFailure("not implemented")
}
func submit<T>(_ task: @escaping () throws -> T) -> EventLoopFuture<T> {
preconditionFailure("not implemented")
}
@discardableResult
func scheduleTask<T>(deadline: NIODeadline, _ task: @escaping () throws -> T) -> Scheduled<T> {
preconditionFailure("not implemented")
}
@discardableResult
func scheduleTask<T>(in: TimeAmount, _ task: @escaping () throws -> T) -> Scheduled<T> {
preconditionFailure("not implemented")
}
func preconditionInEventLoop(file: StaticString, line: UInt) {
preconditionFailure("not implemented")
}
func preconditionNotInEventLoop(file: StaticString, line: UInt) {
preconditionFailure("not implemented")
}
var _succeededVoidFuture: EventLoopFuture<Void>?
func makeSucceededVoidFuture() -> EventLoopFuture<Void> {
guard self.inEventLoop, let voidFuture = self._succeededVoidFuture else {
return self.makeSucceededFuture(())
}
return voidFuture
}
init() {
self._succeededVoidFuture = EventLoopFuture(eventLoop: self, value: (), file: "n/a", line: 0)
}
func shutdownGracefully(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) {
self._succeededVoidFuture = nil
queue.async {
callback(nil)
}
}
}
fileprivate class EventLoopWithoutPreSucceededFuture: EventLoop {
var inEventLoop: Bool {
return true
}
func execute(_ task: @escaping () -> Void) {
preconditionFailure("not implemented")
}
func submit<T>(_ task: @escaping () throws -> T) -> EventLoopFuture<T> {
preconditionFailure("not implemented")
}
@discardableResult
func scheduleTask<T>(deadline: NIODeadline, _ task: @escaping () throws -> T) -> Scheduled<T> {
preconditionFailure("not implemented")
}
@discardableResult
func scheduleTask<T>(in: TimeAmount, _ task: @escaping () throws -> T) -> Scheduled<T> {
preconditionFailure("not implemented")
}
func preconditionInEventLoop(file: StaticString, line: UInt) {
preconditionFailure("not implemented")
}
func preconditionNotInEventLoop(file: StaticString, line: UInt) {
preconditionFailure("not implemented")
}
func shutdownGracefully(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) {
queue.async {
callback(nil)
}
}
}

View File

@ -21,11 +21,11 @@ services:
- MAX_ALLOCS_ALLOWED_1000_addHandlers=47050
- MAX_ALLOCS_ALLOWED_1000_reqs_1_conn=30540
- MAX_ALLOCS_ALLOWED_1000_tcpbootstraps=4100
- MAX_ALLOCS_ALLOWED_1000_tcpconnections=181010
- MAX_ALLOCS_ALLOWED_1000_tcpconnections=180010
- MAX_ALLOCS_ALLOWED_1000_udp_reqs=16050
- MAX_ALLOCS_ALLOWED_1000_udpbootstraps=2000
- MAX_ALLOCS_ALLOWED_1000_udpconnections=103050
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=478050
- MAX_ALLOCS_ALLOWED_1000_udpconnections=102050
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=476050
- MAX_ALLOCS_ALLOWED_bytebuffer_lots_of_rw=2100
- MAX_ALLOCS_ALLOWED_creating_10000_headers=100 # 5.2 improvement 10000
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=2000
@ -37,7 +37,7 @@ services:
- MAX_ALLOCS_ALLOWED_modifying_1000_circular_buffer_elements=50
- MAX_ALLOCS_ALLOWED_modifying_byte_buffer_view=2010 # 5.2 improvement 4000
- MAX_ALLOCS_ALLOWED_ping_pong_1000_reqs_1_conn=4440
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=210050
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=200050
- MAX_ALLOCS_ALLOWED_schedule_10000_tasks=90050
- MAX_ALLOCS_ALLOWED_scheduling_10000_executions=20150
- MAX_ALLOCS_ALLOWED_udp_1000_reqs_1_conn=16250

View File

@ -21,11 +21,11 @@ services:
- MAX_ALLOCS_ALLOWED_1000_addHandlers=47050
- MAX_ALLOCS_ALLOWED_1000_reqs_1_conn=30540
- MAX_ALLOCS_ALLOWED_1000_tcpbootstraps=4100
- MAX_ALLOCS_ALLOWED_1000_tcpconnections=180010
- MAX_ALLOCS_ALLOWED_1000_tcpconnections=179010
- MAX_ALLOCS_ALLOWED_1000_udp_reqs=16050
- MAX_ALLOCS_ALLOWED_1000_udpbootstraps=2000
- MAX_ALLOCS_ALLOWED_1000_udpconnections=102050
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=473050
- MAX_ALLOCS_ALLOWED_1000_udpconnections=101050
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=471050
- MAX_ALLOCS_ALLOWED_bytebuffer_lots_of_rw=2100
- MAX_ALLOCS_ALLOWED_creating_10000_headers=100
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=2000
@ -37,7 +37,7 @@ services:
- MAX_ALLOCS_ALLOWED_modifying_1000_circular_buffer_elements=50
- MAX_ALLOCS_ALLOWED_modifying_byte_buffer_view=2010
- MAX_ALLOCS_ALLOWED_ping_pong_1000_reqs_1_conn=4440
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=200500 #5.3 improvement 210050
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=190500
- MAX_ALLOCS_ALLOWED_schedule_10000_tasks=90050
- MAX_ALLOCS_ALLOWED_scheduling_10000_executions=20150
- MAX_ALLOCS_ALLOWED_udp_1000_reqs_1_conn=16250

View File

@ -21,11 +21,11 @@ services:
- MAX_ALLOCS_ALLOWED_1000_addHandlers=47050
- MAX_ALLOCS_ALLOWED_1000_reqs_1_conn=31990
- MAX_ALLOCS_ALLOWED_1000_tcpbootstraps=3100
- MAX_ALLOCS_ALLOWED_1000_tcpconnections=189050
- MAX_ALLOCS_ALLOWED_1000_tcpconnections=188050
- MAX_ALLOCS_ALLOWED_1000_udp_reqs=18050
- MAX_ALLOCS_ALLOWED_1000_udpbootstraps=2000
- MAX_ALLOCS_ALLOWED_1000_udpconnections=108050
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=950050
- MAX_ALLOCS_ALLOWED_1000_udpconnections=107050
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=948050
- MAX_ALLOCS_ALLOWED_bytebuffer_lots_of_rw=2100
- MAX_ALLOCS_ALLOWED_creating_10000_headers=10100
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=2000
@ -37,7 +37,7 @@ services:
- MAX_ALLOCS_ALLOWED_modifying_1000_circular_buffer_elements=50
- MAX_ALLOCS_ALLOWED_modifying_byte_buffer_view=6010
- MAX_ALLOCS_ALLOWED_ping_pong_1000_reqs_1_conn=4500
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=230050
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=220050
- MAX_ALLOCS_ALLOWED_schedule_10000_tasks=90050
- MAX_ALLOCS_ALLOWED_scheduling_10000_executions=20150
- MAX_ALLOCS_ALLOWED_udp_1000_reqs_1_conn=18250

View File

@ -21,11 +21,11 @@ services:
- MAX_ALLOCS_ALLOWED_1000_addHandlers=47050
- MAX_ALLOCS_ALLOWED_1000_reqs_1_conn=30540
- MAX_ALLOCS_ALLOWED_1000_tcpbootstraps=3100
- MAX_ALLOCS_ALLOWED_1000_tcpconnections=181050
- MAX_ALLOCS_ALLOWED_1000_tcpconnections=180050
- MAX_ALLOCS_ALLOWED_1000_udp_reqs=16050
- MAX_ALLOCS_ALLOWED_1000_udpbootstraps=2000
- MAX_ALLOCS_ALLOWED_1000_udpconnections=103050
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=475050
- MAX_ALLOCS_ALLOWED_1000_udpconnections=102050
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=473050
- MAX_ALLOCS_ALLOWED_bytebuffer_lots_of_rw=2100
- MAX_ALLOCS_ALLOWED_creating_10000_headers=10100
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=2000
@ -37,7 +37,7 @@ services:
- MAX_ALLOCS_ALLOWED_modifying_1000_circular_buffer_elements=50
- MAX_ALLOCS_ALLOWED_modifying_byte_buffer_view=6010
- MAX_ALLOCS_ALLOWED_ping_pong_1000_reqs_1_conn=4440
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=210050
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=200050
- MAX_ALLOCS_ALLOWED_schedule_10000_tasks=90050
- MAX_ALLOCS_ALLOWED_scheduling_10000_executions=20150
- MAX_ALLOCS_ALLOWED_udp_1000_reqs_1_conn=16250