1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Swift のコード実装差によるコストを time で計測する

Last updated at Posted at 2025-01-07

先日、次のような記事を投稿しました。今回は、その記事の続きです。計測環境を少しまじめに設定しました。

本記事の計測について

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

計測対象は 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

一回だけの計測だとノイズも含まれるので、この計測を複数回実行して、その平均を計算するシェルスクリプトを作りました。

measure.sh
#!/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 の生成コストを計測します。

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 は1つの DateFormatter を利用します。

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

これらコードは次のように最適化あり・なしの二種類でビルドして、計測関数に渡しました。

# 最適化なし
% 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 に公開しました。ご自身でも計測したい方はご利用ください。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?