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

# はじめに

[NotificationCenter](https://developer.apple.com/documentation/foundation/notificationcenter)を利用した通知が、なんとなく遅いイメージはありませんか？
果たして本当に通知が遅いのかどうかを、[PublishSubject](https://github.com/ReactiveX/RxSwift/blob/5.0.0/RxSwift/Subjects/PublishSubject.swift)と[PublishRelay](https://github.com/ReactiveX/RxSwift/blob/5.0.0/RxRelay/PublishRelay.swift)と比較しながら見ていこうと思います。
ちなみにパフォーマンスチェックを行う環境は以下になります。

```
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の場合](#optimization-level--os%E3%81%AE%E5%A0%B4%E5%90%88)という項目を追加し、再度結論を出しているので一読いただけますと幸いです。

# パフォーマンスチェック

PublishSubject、PublishRelay、NotificationCenterの通知に関して、いくつかの観点でパフォーマンスチェックしようと思います。
パフォーマンスチェックには、XCTestの[measureMetrics](https://developer.apple.com/documentation/xctest/xctestcase/1496277-measuremetrics?language=objc)を利用します。

## 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`は利用せずにインスタンス化したものを利用します。

```swift: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()
        }
    }
}
```

```swift: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()
        }
    }
}
```

```swift: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](https://developer.apple.com/documentation/foundation/notification/1779652-userinfo)が**[String: Int]**であることとします。

```swift: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()
        }
    }
}
```

```swift: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()
        }
    }
}
```

```swift: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](https://developer.apple.com/documentation/foundation/notification/1779652-userinfo)に渡すことになると思います。
[object](https://developer.apple.com/documentation/foundation/notification/1779839-object)には送信元のオブジェクトを渡しますが、それ以外のオブジェクトを渡すこともできます。
objectを利用した場合はどのような結果になるでしょうか。

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

```swift: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`を利用した場合はどのような結果になるでしょうか。

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

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

```swift: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()
        }
    }
}
```

```swift: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()
        }
    }
}
```

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

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

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