Skip to content
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

Lock-free disposal. #107

Merged
merged 4 commits into from
Dec 1, 2016
Merged

Lock-free disposal. #107

merged 4 commits into from
Dec 1, 2016

Conversation

andersio
Copy link
Member

@andersio andersio commented Nov 23, 2016

Disposable is crucial part of the signal resource management, especially in transformed signals and SignalProducers.

Previous attempt: ReactiveCocoa/ReactiveCocoa#2463

This PR focuses on just Disposable. All disposables have a DisposableState, which exposes accessors built on top of OSAtomic primitives. DisposableState is made a struct so that the state is embedded into the containing class without extra indirection.

This PR introduces a lock-free primitive UnsafeAtomicState, and refactored all disposable types to use it. Note that the primitive is marked with Unsafe as it requires manual memory management.

Note that while CompositeDisposable and SerialDisposable still use Atomic for storing their children, they may still benefit from the fast atomic boolean checks.

The result is pretty impressive. For 1000000 iterations of disposable.dispose() (just a CAS) with whole module optimization, lock-free SimpleDisposable completed in just 0.011 seconds (12% SD), while Atomic<Bool> took 0.233 sec (4% SD).

Less joules. 😸

--

This includes also a 1.5-2x faster Bag.forEach overload under -Owholemodule and -Ounchecked.

@andersio
Copy link
Member Author

andersio commented Nov 24, 2016

DisposablePerfTests.swift
A short synthetic test.

1000000 iterations of disposal* -Onone -Owholemodule
Lock-free, macOS 0.017 sec (4%) 0.013 sec (5%)
Lock-free, iOS 0.022 sec (4%) 0.018 sec (2%)
Atomic, macOS 0.267 sec (4%) 0.234 sec (2%)
Atomic, iOS 0.259 sec (9%) 0.279 sec# (18%)
Speedup, macOS 15.1x 18.0x
Speedup, iOS 11.6x 15.5x#

macOS: 2.4 GHz Intel Core i5-4258U, macOS 10.12.1, Xcode 8.1
iOS: iPhone SE

* dispose() of the old SimpleDisposable is emulated as swap(true).
# Somehow it is slower...

@andersio
Copy link
Member Author

andersio commented Nov 26, 2016

I've factored out the lock-free logic, and wrapped it with a finite state machine-ish interface. It should be at least reusable in Signal for handling interruption. (Edit: #123 removed the lock)

It is a little bit faster too, with the inlining annotation in place. With the same test and configuration, it completes in 0.010 sec (10%).

* Transitions are represented as protocol extensions. Tried tuples and type parameter, but both have a nontrivial cost at runtime.

@andersio andersio force-pushed the disposable-lockfree branch 2 times, most recently from 5ee7e49 to 9fbf8c2 Compare November 26, 2016 12:12
@andersio
Copy link
Member Author

andersio commented Nov 26, 2016

Comparison across branches. The test case is sort of simulating a fast scrolling table view, which gets a bunch of composed properties (2 signals per transform) rebinding agains the cells.

master: 1.164 sec (3%)
Atomic #124: 1.081 sec (2%)
This PR: 0.995 sec (2%)

	func testProducerStart() {
		let property = MutableProperty(1).map { $0 + 1 }.map { $0 + 1 }.map { $0 + 1 }
		let token = Lifetime.Token()
		let lifetime = Lifetime(token)

		var i = 0
		let target = BindingTarget(lifetime: lifetime, setter: { i = $0 })

		measure {
			for i in 0 ..< 10000 {
				let d = target <~ property
				d.dispose()
			}
		}
	}

@NachoSoto
Copy link
Member

Awesome work!!

@NachoSoto NachoSoto merged commit 9ccda34 into master Dec 1, 2016
@NachoSoto NachoSoto deleted the disposable-lockfree branch December 1, 2016 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants