NotificationCenterの通知は果たして本当に遅いのだろうか?[修正版]


はじめに

NotificationCenterを利用した通知が、なんとなく遅いイメージはありませんか?
果たして本当に通知が遅いのかどうかを、PublishSubjectPublishRelayと比較しながら見ていこうと思います。
ちなみにパフォーマンスチェックを行う環境は以下になります。

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時にobjectuserInfonilであることとします。
また、通知の範囲は計測のメソッド内だけなので、NotificationCenter.defaultは利用せずにインスタンス化したものを利用します。

PublishSubject
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()
        }
    }
}
PublishRelay
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()
        }
    }
}
NotificationCenter
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]であることとします。

PublishSubject
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()
        }
    }
}
PublishRelay
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()
        }
    }
}
NotificationCenter
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を利用した場合はどのような結果になるでしょうか。

NotificationCenter
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をラップして計測します。

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を監視登録して、本当に遅くなるのかを確認します。

NotificationCenter
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個の監視登録をしてから通知します。

PublishSubject
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()
        }
    }
}
PublishRelay
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()
        }
    }
}
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

        (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を通知できるようにしてみます。

NotificationCenter
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を利用した場合は暗黙的に監視登録されているものがあるため遅い
  • 利用範囲に合わせてインスタンス化をした場合はPublishSubjectPublishRelayと比べても速い 利用範囲に合わせてインスタンス化をした場合かつOptimization Level -O0の場合はPublishSubjectPublishRelayと比べても速い

  • 利用範囲に合わせてインスタンス化をしても、Optimization Level -Osの場合はPublishSubjectPublishRelayの方が約3倍速い