はじめに
NotificationCenterを利用した通知が、なんとなく遅いイメージはありませんか?
果たして本当に通知が遅いのかどうかを、PublishSubjectとPublishRelayと比較しながら見ていこうと思います。
ちなみにパフォーマンスチェックを行う環境は以下になります。
Mac mini 2018
Intel Core i7 3.2 GHz
DDR4 2667 MHz 32 GB
SSD 512 GB
iPhone XR 12.2 Simulator
Xcode 10.2.1
Swift 5
RxSwift 5.0.0
はじめにの追記(2019/05/25)
2019/05/24時点の記事ではNotificationCenterの通知がPublishSubjectなどと比べて速い
という結論を出していましたが、Optimization Level -O0
での検証しかしておらず、結論を出すには不十分な状態での記事公開となってしまっておりました。申し訳ございません。
Optimization Level -Osの場合という項目を追加し、再度結論を出しているので一読いただけますと幸いです。
パフォーマンスチェック
PublishSubject、PublishRelay、NotificationCenterの通知に関して、いくつかの観点でパフォーマンスチェックしようと思います。
パフォーマンスチェックには、XCTestのmeasureMetricsを利用します。
Optimization Level -O0の場合
CocoaPodsを利用し、特にビルド設定をいじらずにテストを実行した場合、importしているframeworkはOptimization Level -O0
でビルドされた状態になると思います。
まずは、その状態で計測をしていきます。
Generic ArgumentがVoidの場合
ここでは100000回通知し、その都度監視先ではcountに1を足しつつ、countが100000になった場合は計測を終了する実装になっています。
NotificationCenterにVoidを通知するという概念はないので、post時にobject
とuserInfo
がnilであることとします。
また、通知の範囲は計測のメソッド内だけなので、NotificationCenter.default
は利用せずにインスタンス化したものを利用します。
func test_PublishSubject_Void_performance() {
let measurePerformance: (@escaping () -> Void) -> Void = { completion in
let subject = PublishSubject<Void>()
let to: Int = 100000
var count: Int = 0
_ = subject
.subscribe(onNext: {
count += 1
if count == to {
completion()
}
})
(0..<to).forEach { _ in
subject.onNext(())
}
}
measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) {
measurePerformance {
self.stopMeasuring()
}
}
}
func test_PublishRelay_Void_performance() {
let measurePerformance: (@escaping () -> Void) -> Void = { completion in
let relay = PublishRelay<Void>()
let to: Int = 100000
var count: Int = 0
_ = relay
.subscribe(onNext: {
count += 1
if count == to {
completion()
}
})
(0..<to).forEach { _ in
relay.accept(())
}
}
measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) {
measurePerformance {
self.stopMeasuring()
}
}
}
func test_NotificationCenter_Void_performance() {
let measurePerformance: (@escaping () -> Void) -> Void = { completion in
let nc = NotificationCenter()
let name = Notification.Name("performance-test")
let to: Int = 100000
var count: Int = 0
_ = nc
.addObserver(forName: name,
object: nil,
queue: nil,
using: { _ in
count += 1
if count == to {
completion()
}
})
(0..<to).forEach { _ in
nc.post(name: name, object: nil)
}
}
measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) {
measurePerformance {
self.stopMeasuring()
}
}
}
計測結果
PublishSubject | PublishRelay | NotificationCenter | |
---|---|---|---|
1 | 0.272 sec | 0.276 sec | 0.151 sec |
2 | 0.284 sec | 0.280 sec | 0.153 sec |
3 | 0.276 sec | 0.280 sec | 0.151 sec |
4 | 0.278 sec | 0.284 sec | 0.147 sec |
5 | 0.274 sec | 0.280 sec | 0.148 sec |
PublishRelayはPublishSubjectをラップしているため、あまり数値に大きな差は見られません。
ところがNotificationCenterが約半分の時間で完了しています。
Generic ArgumentがIntの場合
ここでは100000回通知し、監視先で受け取った値が100000になった場合は計測を終了する実装になっています。
NotificationCenterにIntを通知するという概念はないので、post時にuserInfoが**[String: Int]**であることとします。
func test_PublishSubject_Int_performance() {
let measurePerformance: (@escaping () -> Void) -> Void = { completion in
let subject = PublishSubject<Int>()
let to: Int = 100000
_ = subject
.subscribe(onNext: {
if $0 == to {
completion()
}
})
(0..<to).forEach {
subject.onNext($0)
}
}
measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) {
measurePerformance {
self.stopMeasuring()
}
}
}
func test_PublishRelay_Int_performance() {
let measurePerformance: (@escaping () -> Void) -> Void = { completion in
let relay = PublishRelay<Int>()
let to: Int = 100000
_ = relay
.subscribe(onNext: {
if $0 == to {
completion()
}
})
(0..<to).forEach {
relay.accept($0)
}
}
measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) {
measurePerformance {
self.stopMeasuring()
}
}
}
func test_NotificationCenter_Int_performance() {
let measurePerformance: (@escaping () -> Void) -> Void = { completion in
let nc = NotificationCenter()
let name = Notification.Name("performance-test")
let key = "user-info-key"
let to: Int = 100000
_ = nc
.addObserver(forName: name,
object: nil,
queue: nil,
using: {
if let value = $0.userInfo?[key] as? Int, value == to {
completion()
}
})
(0..<to).forEach {
nc.post(name: name, object: nil, userInfo: [key: $0])
}
}
measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) {
measurePerformance {
self.stopMeasuring()
}
}
}
計測結果
PublishSubject | PublishRelay | NotificationCenter | |
---|---|---|---|
1 | 0.264 sec | 0.273 sec | 0.343 sec |
2 | 0.272 sec | 0.273 sec | 0.329 sec |
3 | 0.267 sec | 0.272 sec | 0.329 sec |
4 | 0.275 sec | 0.274 sec | 0.330 sec |
5 | 0.270 sec | 0.271 sec | 0.348 sec |
NotificationCenterは、PublishSubjectやPublishRelayと比べて遅い結果となりました。
post時にobjectを利用した場合
NotificationCenterのpost時、追加情報はuserInfoに渡すことになると思います。
objectには送信元のオブジェクトを渡しますが、それ以外のオブジェクトを渡すこともできます。
objectを利用した場合はどのような結果になるでしょうか。
func test_NotificationCenter_Int_with_object_performance() {
let measurePerformance: (@escaping () -> Void) -> Void = { completion in
let nc = NotificationCenter()
let name = Notification.Name("performance-test")
let to: Int = 100000
_ = nc
.addObserver(forName: name,
object: nil,
queue: nil,
using: {
if let value = $0.object as? Int, value == to {
completion()
}
})
(0..<to).forEach {
nc.post(name: name, object: $0)
}
}
measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) {
measurePerformance {
self.stopMeasuring()
}
}
}
計測結果
userInfo | object | |
---|---|---|
1 | 0.343 sec | 0.186 sec |
2 | 0.329 sec | 0.180 sec |
3 | 0.329 sec | 0.184 sec |
4 | 0.330 sec | 0.175 sec |
5 | 0.348 sec | 0.182 sec |
PublishSubject | PublishRelay | NotificationCenter | |
---|---|---|---|
1 | 0.264 sec | 0.273 sec | 0.186 sec |
2 | 0.272 sec | 0.273 sec | 0.180 sec |
3 | 0.267 sec | 0.272 sec | 0.184 sec |
4 | 0.275 sec | 0.274 sec | 0.175 sec |
5 | 0.270 sec | 0.271 sec | 0.182 sec |
objectを利用した場合、userInfoと比べて約60%の時間で計測が完了しました。
PublishSubjectやPublishRelayと比べても速い結果となりました。
NotificationCenterをType-safeにラップした場合
通知するとき
と監視をして値を受け取るとき
の型を合わせるために、以下のようにNotificationCenterをラップして計測します。
final class TypeSafeNotificationCenter<T> {
private let nc = NotificationCenter()
private let name = Notification.Name("performance-test")
func addObserver(using: @escaping (T) -> Void) -> NSObjectProtocol {
return nc.addObserver(forName: name,
object: nil,
queue: nil,
using: { if let v = $0 as? T { using(v) } })
}
func post(_ value: T) {
nc.post(name: name, object: value)
}
}
func test_type_safe_NotificationCenter_Int_performance() {
let measurePerformance: (@escaping () -> Void) -> Void = { completion in
let nc = TypeSafeNotificationCenter<Int>()
let to: Int = 100000
_ = nc
.addObserver {
if $0 == to {
completion()
}
}
(0...to).forEach {
nc.post($0)
}
}
measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) {
measurePerformance {
self.stopMeasuring()
}
}
}
計測結果
Normal | Type-safe | |
---|---|---|
1 | 0.186 sec | 0.178 sec |
2 | 0.180 sec | 0.174 sec |
3 | 0.184 sec | 0.173 sec |
4 | 0.175 sec | 0.177 sec |
5 | 0.182 sec | 0.174 sec |
ラップした場合でも、特に大きな差はない結果となりました。
NotificationCenterのdefaultを利用した場合
それでは、NotificationCenterを利用範囲に合わせてインスタンス化したものではなく、NotificationCenter.default
を利用した場合はどのような結果になるでしょうか。
final class TypeSafeNotificationCenter<T> {
private let nc = NotificationCenter.default
...
}
計測結果
init() | default | |
---|---|---|
1 | 0.178 sec | 0.512 sec |
2 | 0.174 sec | 0.531 sec |
3 | 0.173 sec | 0.526 sec |
4 | 0.177 sec | 0.518 sec |
5 | 0.174 sec | 0.525 sec |
数値が約3倍になっています。
NotificationCenter.default
は暗黙的に複数の監視登録がされているため、遅くなっていると考えられます。
NotificationCenterは遅いというイメージは、この場合に該当しているのかもしれません。
100個のNotification.NameをaddObserverしてから通知した場合
それでは、インスタンス化したNotificaionCenterに100個のNotification.Nameを監視登録して、本当に遅くなるのかを確認します。
func test_NotificationCenter_Int_performance() {
let nc = NotificationCenter()
(0..<100).forEach {
nc.addObserver(forName: Notification.Name("performance-test-\($0)"),
object: nil,
queue: nil,
using: { _ in })
}
let measurePerformance: (@escaping () -> Void) -> Void = { completion in
let name = Notification.Name("performance-test")
let to: Int = 100000
_ = nc
.addObserver(forName: name,
object: nil,
queue: nil,
using: {
if let value = $0.object as? Int, value == to {
completion()
}
})
(0..<to).forEach {
nc.post(name: name, object: $0)
}
}
measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) {
measurePerformance {
self.stopMeasuring()
}
}
}
計測結果
default | init() | |
---|---|---|
1 | 0.512 sec | 0.550 sec |
2 | 0.531 sec | 0.556 sec |
3 | 0.526 sec | 0.556 sec |
4 | 0.518 sec | 0.561 sec |
5 | 0.525 sec | 0.555 sec |
インスタンス化したNotificaionCenterでも遅くなりました。
NotificationCenter.default
には暗黙的に複数の監視登録されていると考えても、あながち間違ってはいなさそうです。
別途10個の監視登録をした場合
次に、PublishSubject、PublishRelayとNotificationCenterに別途10個の監視登録をしてから通知します。
func test_PublishSubject_Int_performance() {
let measurePerformance: (@escaping () -> Void) -> Void = { completion in
let subject = PublishSubject<Int>()
let to: Int = 100000
(0..<10).forEach { _ in
_ = subject.subscribe()
}
_ = subject
.subscribe(onNext: {
if $0 == to {
completion()
}
})
(0...to).forEach {
subject.onNext($0)
}
}
measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) {
measurePerformance {
self.stopMeasuring()
}
}
}
func test_PublishRelay_Int_performance() {
let measurePerformance: (@escaping () -> Void) -> Void = { completion in
let relay = PublishRelay<Int>()
let to: Int = 100000
(0..<10).forEach { _ in
_ = relay.subscribe()
}
_ = relay
.subscribe(onNext: {
if $0 == to {
completion()
}
})
(0...to).forEach {
relay.accept($0)
}
}
measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) {
measurePerformance {
self.stopMeasuring()
}
}
}
final class TypeSafeNotificationCenter<T> {
private let nc = NotificationCenter()
private let name = Notification.Name("performance-test")
func addObserver(using: @escaping (T) -> Void) -> NSObjectProtocol {
return nc.addObserver(forName: name,
object: nil,
queue: nil,
using: { if let v = $0 as? T { using(v) } })
}
func post(_ value: T) {
nc.post(name: name, object: value)
}
}
func test_type_safe_NotificationCenter_Int_performance() {
let measurePerformance: (@escaping () -> Void) -> Void = { completion in
let nc = TypeSafeNotificationCenter<Int>()
let to: Int = 100000
(0..<10).forEach { _ in
_ = nc.addObserver(using: { _ in })
}
_ = nc
.addObserver {
if $0 == to {
completion()
}
}
(0...to).forEach {
nc.post($0)
}
}
measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) {
measurePerformance {
self.stopMeasuring()
}
}
}
計測結果
PublishSubject | PublishRelay | NotificationCenter | |
---|---|---|---|
1 | 1.243 sec | 1.235 sec | 0.885 sec |
2 | 1.220 sec | 1.225 sec | 0.879 sec |
3 | 1.238 sec | 1.223 sec | 0.894 sec |
4 | 1.229 sec | 1.259 sec | 0.856 sec |
5 | 1.262 sec | 1.284 sec | 0.891 sec |
この場合でも、NotificationCenterの方が速い結果となりました。
NotificationCenterでもErrorを扱えるようにした場合
PublishSubjectはErrorも通知できるので、NotificationCenterをラップしたクラスでもErrorを通知できるようにしてみます。
final class TypeSafeNotificationCenter<T> {
private let nc = NotificationCenter()
private let name = Notification.Name("performance-test")
func addObserver(onSuccess: ((T) -> Void)? = nil,
onError: ((Error) -> Void)? = nil) -> NSObjectProtocol {
let using: (Notification) -> Void = { notification in
guard let v = notification.object as? Result<T, Error> else {
return
}
do {
try onSuccess?(v.get())
} catch {
onError?(error)
}
}
return nc.addObserver(forName: name,
object: nil,
queue: nil,
using: using)
}
func onSuccess(_ value: T) {
nc.post(name: name, object: Result<T, Error>.success(value))
}
func onError(_ error: Error) {
nc.post(name: name, object: Result<T, Error>.failure(error))
}
}
func test_type_safe_NotificationCenter_Int_performance() {
let measurePerformance: (@escaping () -> Void) -> Void = { completion in
let nc = TypeSafeNotificationCenter<Int>()
let to: Int = 100000
_ = nc
.addObserver(onSuccess: {
if $0 == to {
completion()
}
})
(0...to).forEach {
nc.onSuccess($0)
}
}
measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) {
measurePerformance {
self.stopMeasuring()
}
}
}
計測結果
| | PublishSubject | NotificationCenter |
| :-: | :-: | :-: | :-: |
| 1 | 0.264 sec | 0.220 sec |
| 2 | 0.272 sec | 0.221 sec |
| 3 | 0.267 sec | 0.222 sec |
| 4 | 0.275 sec | 0.222 sec |
| 5 | 0.270 sec | 0.224 sec |
Errorを通知できるようにした場合でも、NotificationCenterの方が速い結果となりました。
Optimization Level -Osの場合
次は、carthageでOptimization Level -Os
でビルドされたframeworkを利用してパフォーマンスチェックを行います。(TypeSafeNotificationCenterもframework化した成果物を利用します)
Optimization Level -O0
のように、100000回通知し監視先で受け取った値が100000になった場合は終了する計測を行います。
計測結果
PublishSubject | PublishRelay | NotificationCenter | |
---|---|---|---|
1 | 0.067 sec | 0.069 sec | 0.197 sec |
2 | 0.068 sec | 0.070 sec | 0.201 sec |
3 | 0.067 sec | 0.069 sec | 0.194 sec |
4 | 0.068 sec | 0.069 sec | 0.196 sec |
5 | 0.069 sec | 0.070 sec | 0.191 sec |
NotificationCenterの計測を完了するまでに、PublishSubjectやPublishRelayと比べて約3倍の時間がかかっている結果となりました。
結論
-
NotificationCenter.default
を利用した場合は暗黙的に監視登録されているものがあるため遅い -
利用範囲に合わせてインスタンス化をした場合は利用範囲に合わせてインスタンス化をした場合かつOptimization Level -O0の場合はPublishSubject
やPublishRelay
と比べても速いPublishSubject
やPublishRelay
と比べても速い -
利用範囲に合わせてインスタンス化をしても、Optimization Level -Osの場合は
PublishSubject
やPublishRelay
の方が約3倍速い