先日、次のような記事を投稿しました。今回は、その記事の続きです。計測環境を少しまじめに設定しました。
本記事の計測について
本記事で扱う計測は、厳密な計測ではないことに注意してください。実装の差によって「負荷がこれぐらい違うよ」程度の目安として、読んでください。厳密な計測は、厳密な計測結果を知りたいと思ったアナタが計測します。
計測対象は macOS 上のターミナルで実行するシンプルな実行ファイルとします。その実行ファイルを実行して、計算時間、CPU使用率、メモリ使用量を計測します。
計測環境は、MacBook Pro 14インチ 2021 / Apple M1 Pro / メモリ 32 GB / macOS Sonoma 14.6.1 で行いました。計測ツールは前回と異なり GNU Time、対象コードのビルドに 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
GNU Time で計測関数を作る
前回は Python で計測用のコードを作って計測しましたが、今回は time コマンドを利用します。計測用のコマンドなので、精度は期待できます。注意点として、macOS には2つの time が存在します(それぞれ何の time なのか分かっていないので、有識者の方々教えてください)。試しに私の環境にインストールされている Node.js のバージョン確認を計測してみました。
% time node -v
v22.11.0
node -v 0.04s user 0.02s system 42% cpu 0.123 total
% /usr/bin/time node -v
v22.11.0
0.11 real 0.03 user 0.01 sys
しかし、これらは計測したいデータが取得できないので、さらに第三の time となる GNU Time を利用しました。homebrew からインストールできます。
brew install gnu-time
実行すると、時間の他に、CPU 利用率とメモリ利用量なども取得できます。
% gtime node -v
v22.11.0
0.03user 0.02system 0:00.15elapsed 37%CPU (0avgtext+0avgdata 19264maxresident)k
0inputs+0outputs (1006major+1689minor)pagefaults 0swaps
一回だけの計測だとノイズも含まれるので、この計測を複数回実行して、その平均を計算するシェルスクリプトを作りました。
#!/bin/bash
# デフォルト値
EXECUTABLE=""
RUN_COUNT=1
OUTPUT_FILE="output.csv"
OUTPUT_DIR="dist"
# 使用方法
usage() {
echo "Usage: $0 -e <executable> [-c <run_count>] [-o <output_file>]"
echo " -e: 実行するファイルのパス (必須)"
echo " -c: 実行回数 (デフォルト: $RUN_COUNT)"
echo " -o: 出力CSVファイル名 (デフォルト: output.csv)"
exit 1
}
# gtime のコマンドが存在するか確認
if ! command -v gtime &> /dev/null; then
echo "gtime コマンドが見つかりません。GNU time が必要です。"
echo "Homebrew で gnu-time をインストールしてください。"
echo "brew install gnu-time"
exit 1
fi
# 引数の解析
while getopts "e:c:o:" opt; do
case $opt in
e) EXECUTABLE="$OPTARG" ;;
c) RUN_COUNT="$OPTARG" ;;
o) OUTPUT_FILE="$OPTARG" ;;
*) usage ;;
esac
done
# 実行ファイルが指定されていない場合はエラー
if [[ -z "$EXECUTABLE" ]]; then
echo "Error: 実行ファイルが指定されていません"
usage
fi
# 値を保存する配列を初期化
real_time_list=()
user_time_list=()
sys_time_list=()
memory_list=()
cpu_usage_list=()
if [ ! -d "$OUTPUT_DIR" ]; then
mkdir $OUTPUT_DIR
fi
# CSVのヘッダーを書き込み
echo "# Elapsed Time(sec),System Time(sec),User Time(sec),CPU Usage(%),MaxMemory(MB)" > "$OUTPUT_DIR/$OUTPUT_FILE"
# 指定回数実行
for (( i=1; i<=RUN_COUNT; i++ )); do
# /usr/bin/timeで実行し、出力を解析
OUTPUT=$(gtime -f "%e %U %S %M %P" "$EXECUTABLE" 2>&1 >/dev/null)
# 出力結果を分割して取得
REAL_TIME=$(echo "$OUTPUT" | awk '{print $1}')
USER_TIME=$(echo "$OUTPUT" | awk '{print $2}')
SYS_TIME=$(echo "$OUTPUT" | awk '{print $3}')
MEMORY_RAW=$(echo "$OUTPUT" | awk '{print $4}')
CPU_USAGE_RAW=$(echo "$OUTPUT" | awk '{print $5}')
# メモリを MB 単位に変換する
MEMORY_MB=$(echo "scale=2; $MEMORY_RAW / 1024" | bc)
# CPU_USAGE_RAWから単位(%)を取り除く
CPU_USAGE=$(echo "$CPU_USAGE_RAW" | sed 's/%//')
# 結果を配列に格納
real_time_list+=("$REAL_TIME")
user_time_list+=("$USER_TIME")
sys_time_list+=("$SYS_TIME")
memory_list+=("$MEMORY_MB")
cpu_usage_list+=("$CPU_USAGE")
# 結果をCSVに追加
echo "$REAL_TIME,$USER_TIME,$SYS_TIME,$CPU_USAGE,$MEMORY_MB" >> "$OUTPUT_DIR/$OUTPUT_FILE"
done
echo "結果が $OUTPUT_FILE に保存されました。"
# 平均を計算する関数
calculate_mean() {
local values=("$@") # 配列引数
local sum=0
local count=${#values[@]}
# 平均を計算
for v in "${values[@]}"; do
sum=$(echo "$sum + $v" | bc)
done
local mean=$(echo "scale=2; $sum / $count" | bc)
# 結果を返す
printf "%.2f" "$mean"
}
# 各値の平均を計算
real_mean=$(calculate_mean "${real_time_list[@]}")
user_mean=$(calculate_mean "${user_time_list[@]}")
sys_mean=$(calculate_mean "${sys_time_list[@]}")
memory_mean=$(calculate_mean "${memory_list[@]}")
cpu_mean=$(calculate_mean "${cpu_usage_list[@]}")
# 結果を表示
echo "==== 平均結果 ===="
echo "経過時間 (REAL_TIME): 平均 $real_mean 秒"
echo "ユーザーモード時間 (USER_TIME): 平均 $user_mean 秒"
echo "システムモード時間 (SYS_TIME): 平均 $sys_mean 秒"
echo "メモリ使用量 (MEMORY): 平均 $memory_mean MB"
echo "CPU利用率 (CPU_USAGE): 平均 $cpu_mean %"
このスクリプトは、次のように実行します。
./measure.sh -e {実行ファイル} -c {試行回数} -o {試行結果を書き出すcsvの名前}
たとえば、実行例は次のようになります。
% ./measure.sh -e ./code -c 10
結果が output.csv に保存されました。
==== 平均結果 ====
経過時間 (REAL_TIME): 平均 1.75 秒
ユーザーモード時間 (USER_TIME): 平均 1.73 秒
システムモード時間 (SYS_TIME): 平均 0.01 秒
メモリ使用量 (MEMORY): 平均 67.01 MB
CPU利用率 (CPU_USAGE): 平均 99.00 %
DateFormatter のコストを計測する
前回と同様に 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 -Onone code1.swift -o code1
# 最適化あり
% swiftc -O code1.swift -o code1_opt
結果
試行回数を 10 回として、計測したコストの平均値を取得しました。ここで、CPU 使用率は次の式で計算されています。
{\rm CPU}使用率 = \frac{(ユーザー時間+システム時間)}{経過時間}
計算時間 | CPU使用率 | メモリ使用量 | |
---|---|---|---|
code1(最適化なし) | 42.93 秒 | 99.00 % | 150.15 MB |
code1(最適化あり) | 41.60 秒 | 99.00 % | 148.34 MB |
code2(最適化なし) | 2.29 秒 | 99.00 % | 67.89 MB |
code2(最適化あり) | 1.74 秒 | 99.00 % | 66.96 MB |
前回と同様に、DateFormatter の生成コストは高いですね。また、最適化オプションは、今回の恩恵は少ないですが、時間やメモリの削減に効果がありました。
まとめ
計測は興味深いですね。計測スクリプトに CSV 出力をつけたので、次に機会があれば、グラフ化して結果を視覚化するのも面白そうですね。
今回および前回作成したの対象と計測コードを GitHub に公開しました。ご自身でも計測したい方はご利用ください。