Swift の DateFormatter のコストを計測する

Swift のコードレビューをしてるとき、『[DateFormatter](https://developer.apple.com/documentation/foundation/dateformatter) の生成コストは高いから、毎回生成せずに使い回すといいよ』と指摘することがあります。ただ、実際にコストを計測したことがなかったので、実際はどれぐらいなんだろう。ならば、計測しましょう。

## 本記事の計測について

:::note warn
本記事で扱う計測は、厳密な計測ではないことに注意してください。実装の差によって「負荷がこれぐらい違うよ」程度の目安として、読んでください。厳密な計測は、厳密な計測結果を知りたいと思ったアナタが計測します。
:::

計測対象は macOS 上のターミナルで実行するシンプルな実行ファイルとします。その実行ファイルを実行して、計算時間、CPU使用量、メモリ使用量を計測します。

計測環境は、MacBook Pro 14インチ 2021 / Apple M1 Pro / メモリ 32 GB / macOS Sonoma 14.6.1 で行いました。また、計測ツールに Python、対象コードのビルドに swiftc を利用しました。

```shell
% python --version
Python 3.11.2

% swiftc --version
swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4)
Target: arm64-apple-macosx14.0
```

## Python で計測関数を作る

計測対象の実行ファイルを実行して、その計算時間、実行中のCPUとメモリの利用量を計測するツールを Python で作成しました。

```python:measure.py
import psutil
import subprocess
import time
import argparse

def monitor_process(executable_path):
    # 測定対象のプログラムを開始
    process = subprocess.Popen([executable_path], stdout=subprocess.PIPE)

    # 初期値を取得
    initial_cpu = psutil.cpu_percent(interval=0.1)  # 実行前のCPU使用率
    initial_memory = psutil.virtual_memory().used  # 実行前のメモリ使用量 (バイト)

    # 最大値を記録するための変数
    max_cpu_usage = 0
    max_memory_usage = 0

    # 開始時間
    start_time = time.time()

    # プロセスが終了するまでモニタリング
    while process.poll() is None:
        # 現在のCPUとメモリの使用量を取得
        cpu_usage = psutil.cpu_percent(interval=0.1)
        memory_info = psutil.virtual_memory().used

        # 最大値を更新
        max_cpu_usage = max(max_cpu_usage, cpu_usage)
        max_memory_usage = max(max_memory_usage, memory_info)

        # 情報を表示する（計測中は、この表示が負荷となるのでコメントアウトする）
        # print(f"CPU Usage: {cpu_usage}%, Memory Usage: {(memory_info - initial_memory) / (1024 ** 2):.2f} MB")

    # 終了時間
    end_time = time.time()

    # 実行時間を計算
    execution_time = end_time - start_time

    # 結果を表示
    print("\n--- Monitoring Results ---")
    print(f"Execution time: {execution_time:.2f} seconds")
    print(f"Maximum CPU Usage: {max_cpu_usage:.2f} %")
    print(f"Maximum Memory Usage: {(max_memory_usage - initial_memory) / (1024 ** 2):.2f} MB")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Monitor CPU and memory usage of an executable.")
    parser.add_argument("executable", help="Path to the executable file")
    args = parser.parse_args()

    monitor_process(args.executable)
```

この関数を実行するために、`psutil` をインストールしてください。

```shell
pip install psutil
```

計測したい実行ファイルを引数に設定して、計測を行います。

```shell
python measure.py {実行ファイル}
# python measure.py ./code

--- Monitoring Results ---
Execution time: **.** seconds
Maximum CPU Usage: **.** %
Maximum Memory Usage: **.** MB
```

繰り返しになりますが、この実装は厳密な計測ではありません。計測中に他プロセスが動作したら、その影響も受けます。また、この計測ツール自体の実行負荷も影響します。

## DateFormatter のコストを計測する

DateFormatter の生成コストを計測するため、簡単なコードを用意しました。`code1.swift` は毎回 DateFormatter を生成します。

```swift:code1.swift
import Foundation

for _ in 0..<1_000_000 {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
    _ = dateFormatter.string(from: Date())
}
```

一方で、`code2.swift` は１つの DateFormatter を利用します。

```swift:code2.swift
import Foundation

let dateFormatter = DateFormatter()
for _ in 0..<1_000_000 {
    dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
    _ = dateFormatter.string(from: Date())
}
```

これらコードは次のようにビルドして、計測関数に渡しました。

```shell
% swiftc code1.swift -o code1
% swiftc code1.swift -o code2
% python measure.py code1
% python measure.py code2
```

では、実際に計測した結果です。

||計算時間|最大CPU使用量|最大メモリ使用量|
|:--|---|---|---|
|code1| 44.86 秒 | 19.80% | 184.53 MB |
|code2| 2.62 秒 | 14.70% | 7.62 MB |

DateFormatter を逐次生成した code1 は、１つの DateFormatter を利用した code2 と比べると明らかに負荷が大きいですね。実際に計測すると、ここまで差が大きく出るとは驚きです。ただ、時間変換を大量に for ループで処理するのは実アプリではないと思うので、実際にどう実装するかはコードによるかなと思います。

## まとめ

「推測するな、計測せよ」とも言われてますが、実際に計測すると面白いですね。以上、終わり。