Swift のコードレビューをしてるとき、『DateFormatter の生成コストは高いから、毎回生成せずに使い回すといいよ』と指摘することがあります。ただ、実際にコストを計測したことがなかったので、実際はどれぐらいなんだろう。ならば、計測しましょう。
本記事の計測について
本記事で扱う計測は、厳密な計測ではないことに注意してください。実装の差によって「負荷がこれぐらい違うよ」程度の目安として、読んでください。厳密な計測は、厳密な計測結果を知りたいと思ったアナタが計測します。
計測対象は macOS 上のターミナルで実行するシンプルな実行ファイルとします。その実行ファイルを実行して、計算時間、CPU使用量、メモリ使用量を計測します。
計測環境は、MacBook Pro 14インチ 2021 / Apple M1 Pro / メモリ 32 GB / macOS Sonoma 14.6.1 で行いました。また、計測ツールに Python、対象コードのビルドに swiftc を利用しました。
% 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 で作成しました。
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
をインストールしてください。
pip install psutil
計測したい実行ファイルを引数に設定して、計測を行います。
python measure.py {実行ファイル}
# python measure.py ./code
--- Monitoring Results ---
Execution time: **.** seconds
Maximum CPU Usage: **.** %
Maximum Memory Usage: **.** MB
繰り返しになりますが、この実装は厳密な計測ではありません。計測中に他プロセスが動作したら、その影響も受けます。また、この計測ツール自体の実行負荷も影響します。
DateFormatter のコストを計測する
DateFormatter の生成コストを計測するため、簡単なコードを用意しました。code1.swift
は毎回 DateFormatter を生成します。
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
は1つの DateFormatter を利用します。
import Foundation
let dateFormatter = DateFormatter()
for _ in 0..<1_000_000 {
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
_ = dateFormatter.string(from: Date())
}
これらコードは次のようにビルドして、計測関数に渡しました。
% 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 は、1つの DateFormatter を利用した code2 と比べると明らかに負荷が大きいですね。実際に計測すると、ここまで差が大きく出るとは驚きです。ただ、時間変換を大量に for ループで処理するのは実アプリではないと思うので、実際にどう実装するかはコードによるかなと思います。
まとめ
「推測するな、計測せよ」とも言われてますが、実際に計測すると面白いですね。以上、終わり。