【高速プロファイリング】VBScriptの限界を看破する!ミリ秒単位の計測でボトルネックを叩き潰す技術
開発プロジェクトの現場において、VBScriptは「手軽な自動化の切り札」であると同時に、「時限爆弾」にもなり得る。
「なんだか最近、この日次バッチの処理が終わるのが遅い気がする……」
「数万件のCSVを読み込んでいるだけなのに、なぜかCPUが張り付いてフリーズする……」
君がもし、なんとなく動くコードを書き連ねた結果としての「処理遅延」に直面しているなら、それはプロファイリング(性能測定)の欠如が招いた人災だ。なんとなくの勘で `CreateObject` を乱発し、最適化されていないループを回すのは、エンジンオイルを入れずにF1マシンを走らせるようなものだ。
今回は、VBScript環境において正確無比な実行時間を計測し、コードの急所(ボトルネック)を外科手術の如く特定するための「極限のプロファイリング手法」を伝授する。
—
1. なぜ「感覚」頼みのチューニングは失敗するのか
VBScriptのパフォーマンスチューニングにおいて、最大の敵は「COMオブジェクトの境界を跨ぐコスト」と「文字列結合のメモリ再割り当て」だ。
未熟なプログラマーは、「おそらくファイル書き込みが遅いのだろう」と推測でコードを書き換え、無駄なリファクタリングに時間を溶かす。プロフェッショナルは違う。データで語れ。 どの一行が、どの数ミリ秒を消費しているのかをミリ秒単位で暴き出し、最小の労力で最大のパフォーマンスを引き出すのだ。
VBScriptで時間を測る代表的なアプローチとして以下の2つがある。
1. `Timer` 関数: 0時からの経過秒数を単精度浮動小数点数(Single)で返す。手軽だが、日をまたぐ瞬間や、分解能の限界がある。
2. `Win32_PerfFormattedData_PerfOS_System` や `GetTickCount` (API): システム起動からのミリ秒を正確に捉える。本番環境の厳密なプロファイリングには、OSのタイマー精度を直接叩くアプローチ、あるいは安定した `Timer` のラップが不可欠となる。
今回は、実務で最も堅牢かつ導入しやすい、WSH環境を前提とした高精度プロファイラクラスの設計を公開する。
—
2. 【プロダクションコード】コピペで使える「ハイパフォーマンス・プロファイラ」
以下のコードは、単なるタイマー計測にとどまらず、スコープごとの実行時間を蓄積し、ボトルネックを視覚化するためのクラス構造を持つ。実務の業務効率化ツールにそのまま組み込める堅牢性を持たせてある。
‘ ==============================================================================
‘ Script Name: HighPerformanceProfiler.vbs
‘ Description: ミリ秒単位で処理時間を計測し、ボトルネックを特定するプロファイラ
‘ Author: Lead Automation Engineer
‘ ==============================================================================
Option Explicit
‘ プロファイラのインスタンスを作成して実行
Call Main()
Sub Main()
Dim profiler
Set profiler = New CProfiler
‘ — プロファイリング開始 —
profiler.Start()
‘ [ボトルネック・シナリオ 1] 非効率な文字列結合のシミュレーション
profiler.Checkpoint “1. 非効率な文字列結合 (Bad)”
Dim i, badStr
badStr = “”
For i = 1 to 5000
‘ 毎回メモリの再割り当てが発生するため、後半から爆発的に遅くなる
badStr = badStr & “X”
Next
‘ [ボトルネック・シナリオ 2] 配列を使った高速な文字列構築のシミュレーション
profiler.Checkpoint “2. 配列とJoinによる文字列結合 (Good)”
Dim arr(5000)
For i = 1 to 5000
arr(i) = “X”
Next
Dim goodStr
goodStr = Join(arr, “”)
‘ [ボトルネック・シナリオ 3] 無駄なCOMオブジェクト生成のオーバーヘッド
profiler.Checkpoint “3. 外部COMオブジェクト生成コスト”
Dim fso
For i = 1 to 100
‘ ループ内で毎回生成・破棄を繰り返す悪手
Set fso = CreateObject(“Scripting.FileSystemObject”)
Set fso = Nothing
Next
‘ — プロファイリング終了 —
profiler.Stop()
‘ 結果をコンソール出力(ログファイル出力へリダイレクト可能)
WScript.Echo profiler.DumpReport()
End Sub
‘ ==============================================================================
‘ クラス名: CProfiler
‘ 概要: 処理の各区間(Checkpoint)の実行時間を計測・集計するプロファイラ
‘ ==============================================================================
Class CProfiler
Private m_StartTime
Private m_LastTime
Private m_Checkpoints()
Private m_Count
‘ コンストラクタ
Private Sub Class_Initialize()
m_Count = -1
ReDim m_Checkpoints(10)
End Sub
‘ 計測開始
Public Sub Start()
m_StartTime = Timer()
m_LastTime = m_StartTime
End Sub
‘ チェックポイント設定(前回のCheckpointから現在までの時間を計測)
Public Sub Checkpoint(ByVal label)
Dim currentTime, elapsed
currentTime = Timer()
‘ 0時をまたいだ場合の簡易補正(Timer関数特有の対策)
If currentTime < m_LastTime Then
currentTime = currentTime + 86400
End If
elapsed = (currentTime - m_LastTime) 1000 ' ミリ秒換算
m_Count = m_Count + 1
If UBound(m_Checkpoints) < m_Count Then
ReDim Preserve m_Checkpoints(m_Count + 10)
End If
Set m_Checkpoints(m_Count) = New CCheckPointData
m_Checkpoints(m_Count).Label = label
m_Checkpoints(m_Count).ElapsedMs = elapsed
m_LastTime = currentTime
End Sub
' 計測終了
Public Sub Stop()
Call Checkpoint("【計測終了・トータル集計】")
End Sub
' レポート文字列の生成
Public Function DumpReport()
Dim totalTime, i, report
totalTime = 0
report = "==========================================" & vbCrLf
report = report & " VBScript Performance Profiler Report" & vbCrLf
report = report & "==========================================" & vbCrLf
For i = 0 To m_Count
If Not IsEmpty(m_Checkpoints(i)) Then
If m_Checkpoints(i).Label <> “【計測終了・トータル集計】” Then
totalTime = totalTime + m_Checkpoints(i).ElapsedMs
End If
report = report & “[” & FormatNumber(m_Checkpoints(i).ElapsedMs, 2) & ” ms] ” & m_Checkpoints(i).Label & vbCrLf
End If
Next
report = report & “——————————————” & vbCrLf
report = report & “Total Elapsed Time: ” & FormatNumber(totalTime, 2) & ” ms” & vbCrLf
report = report & “==========================================” & vbCrLf
DumpReport = report
End Function
End Class
‘ ==============================================================================
‘ クラス名: CCheckPointData (内部データ保持用)
‘ ==============================================================================
Class CCheckPointData
Public Label
Public ElapsedMs
End Class
—
3. コードから読み解く「真の最適化」の勘所
上記のコードを実行してみると、VBScriptの残酷な現実が数値として目の前に突きつけられるはずだ。
① 文字列結合 (`&`) の罠
ループ内で `str = str & “X”` を行うと、VBScriptは毎回メモリ領域の再確保と文字のコピーを行う。件数が数千件を超えたあたりから、この処理だけで数秒〜数十秒のラグを生む。
対して、`Array` にデータを詰めて最後に `Join` する手法は、メモリ確保が一度で済むため、処理時間が劇的に短縮される(場合によっては10倍以上の差が出る)。
② 外部COMオブジェクトのループ内生成
`CreateObject(“Scripting.FileSystemObject”)` をループ内で回す愚を犯していないか?
COMオブジェクトのインスタンス化は、OSのプロセス境界やCOMランタイムを巻き込むため、非常に「重い」処理である。オブジェクトの生成は必ずループの外で行い、ループ内では使い回す(または用事が済んだら即座に解放する)。これが鉄則だ。
—
4. データベースやファイル連携におけるプロファイリングの注意点
実務でVBScriptを使う場合、多くはExcel操作、CSVの読み書き、あるいはADO(ActiveX Data Objects)経由でのSQL Server / Accessへの接続が絡む。これらをプロファイリングする際の極意を授けよう。
1. I/O待ちの分離:
データベースのクエリ実行時間や、ファイルI/Oの時間は、純粋なVBScriptの演算処理速度とは切り分けて計測すべきだ。ネットワークの遅延やディスクのシークタイムに引きずられているのか、VBScript側のロジックが非効率なのかを `Checkpoint` で明確に切り分けろ。
2. トランザクションと一括処理:
DBへのインサートを1件ずつループで行うのではなく、`Command` オブジェクトやトランザクションを用いた一括処理(Bulk Insert的アプローチ)に切り替える前後にこのプロファイラーを挟めば、どれほどの爆発的効果があるかが一目瞭然となる。
—
5. まとめ:プロファイリングなき自動化は「技術的負債」の温床
動けばいい、というフェーズはプロトタイプで終わらせるべきだ。
本番環境で稼働する業務自動化ツール、特に夜間バッチや大量データ処理を担うスクリプトにおいて、「遅い」というクレームは開発者の信用失墜に直結する。
今回紹介した `CProfiler` クラスをあなたのツールキットに組み込み、感覚ではなく「ミリ秒の根拠」を持ってコードを最適化せよ。
真のプロフェッショナルエンジニアとは、コードの美しさだけでなく、リソースの限界を正確に把握し、最速のソリューションを叩き出す者のことだ。
