こんにちは!今日も一緒にプログラミングの深遠で楽しい世界を探求していきましょう。
Excel VBAなどのマクロから一歩踏み出し、VB.NETでより本格的なアプリケーションを開発し始めると、必ずぶつかる大きな壁があります。それが「なぜかこの処理、重い気がする…」というパフォーマンスの課題です。
アプリの速度を改善(チューニング)するためには、まず「どの処理にどれくらいの時間がかかっているか」を正しく測らなければなりません。しかし、初学者の多くがやってしまいがちなのが、`DateTime.Now`を使った時刻の差分計算です。
実はこれ、プロの現場では「正確なパフォーマンス計測においてはアンチパターン(やってはいけない手法)」とされています。
今回は、なぜ `DateTime.Now` ではダメなのかという構造的な理由から、.NET Framework / .NET Coreに用意された最高精度の計測器`System.Diagnostics.Stopwatch`クラスの正しい使い方、さらにはプロのエンジニアが実践する「ボトルネック(低速の原因)特定の手法」まで、優しく、かつ本質に迫る形で解説します。
ここをクリアすれば、Visual Basic (VB / VB.NET)の基本とパフォーマンスの考え方はバッチリですよ!
—
1. なぜ `DateTime.Now` で処理時間を測ってはいけないのか?
まずは、よく見かける「間違った計測コード」を見てみましょう。
‘ 【避けるべき例】DateTime.Now を使った計測
Dim startTime As DateTime = DateTime.Now
‘ 〜 何らかの重い処理 〜
Dim endTime As DateTime = DateTime.Now
Dim elapsed As TimeSpan = endTime – startTime
Console.WriteLine($”処理時間: {elapsed.TotalMilliseconds} ミリ秒”)
一見、問題なさそうに見えますよね?「開始時刻と終了時刻を引き算しているだけ」だからです。
しかし、これには2つの大きな落とし穴が存在します。
落とし穴①:Windowsの時計の「分解能(精度)」の問題
`DateTime.Now` は、オペレーティングシステム(Windowsなど)のシステム時計を参照しています。このシステム時計は、常に細かく動いているわけではなく、約 15.6 ミリ秒(1/64秒)間隔でしか更新されません。
つまり、実際には「3ミリ秒」で終わった超高速な処理であっても、タイミングによって「0ミリ秒」と表示されたり、「15.6ミリ秒」と表示されたりしてしまうのです。ミリ秒単位の精密な測定には全く耐えられません。
落とし穴②:NTP同期(時刻合わせ)による「時間の歪み」
PCの時計は、バックグラウンドでインターネット上の時刻サーバー(NTP)と同期し、常に微修正されています。計測の最中に「時間を1ミリ秒戻す」という調整が入ると、経過時間がマイナスになったり、不自然に跳ね上がったりする危険性があります。
—
2. 救世主 `System.Diagnostics.Stopwatch` の登場
そこで登場するのが、.NETが提供するパフォーマンス計測の専門家、`Stopwatch`(ストップウォッチ)クラスです。
`Stopwatch` は、システム時計(カレンダーの時刻)を見るのではなく、CPU内部の高分解能パフォーマンスカウンタ(Hardware Performance Counter)を直接読みに行きます。
- 精度: 1マイクロ秒(0.001ミリ秒)以下、ナノ秒レベルの分解能
- 安定性: NTPなどの時刻合わせの影響を一切受けない
まさに「アプリの速度を測るための専用デジタルストップウォッチ」なのです。
—
3. 実戦コード:Stopwatchを使ってボトルネックを特定する
では、実際に動作するVB.NETのコードを書いてみましょう。
以下のプログラムは、CSVデータの読み込みや加工を模した処理の中で、「どのパートが一番重いのか(ボトルネック)」を精密にミリ秒単位で計測するサンプルです。
コンソールアプリケーションを作成し、以下のコードをそのまま貼り付けて実行してみてください。
Imports System
Imports System.Diagnostics ‘ Stopwatchクラスを使用するために必要
Imports System.Threading ‘ Thread.Sleep(擬似遅延)のために使用
Module Program
Sub Main()
Console.WriteLine(“=== パフォーマンス計測処理を開始します ===”)
Console.WriteLine()
‘ 1. Stopwatchのインスタンス化
Dim sw As New Stopwatch()
‘ ————————————————————-
‘ フェーズ1: 擬似的なファイル読み込み処理の計測
‘ ————————————————————-
‘ Start() で計測を開始します(以前の記録をリセットして再スタートする場合は Restart() を使います)
sw.Start()
‘ 擬似的な処理(200ミリ秒停止)
SimulateFileRead()
sw.Stop() ‘ 計測一時停止
‘ Sw.ElapsedMilliseconds で経過ミリ秒を取得
Console.WriteLine($”[フェーズ1] ファイル読み込み完了: {sw.ElapsedMilliseconds} ms”)
‘ ————————————————————-
‘ フェーズ2: 擬似的なデータ加工処理(ループ処理)の計測
‘ ————————————————————-
‘ 前回の計測値をクリアして、ゼロから再スタート
sw.Restart()
‘ 擬似的な重いデータ加工(ループ処理)
SimulateDataProcessing()
sw.Stop()
Console.WriteLine($”[フェーズ2] データ加工完了 : {sw.ElapsedMilliseconds} ms”)
‘ ————————————————————-
‘ フェーズ3: より詳細な時間を計測(TimeSpan構造体を使用)
‘ ————————————————————-
‘ ミリ秒以下の端数まで精度高く取得したい場合
Dim elapsedSpan As TimeSpan = sw.Elapsed
Console.WriteLine($”[詳細ログ] フェーズ2の正確な時間: {elapsedSpan.TotalMilliseconds:F3} ms”)
Console.WriteLine()
Console.WriteLine(“=== 全ての計測が完了しました ===”)
Console.ReadLine()
End Sub
”’
”’
Private Sub SimulateFileRead()
‘ 200ミリ秒間、スレッドを休止させる
Thread.Sleep(200)
End Sub
”’
”’
Private Sub SimulateDataProcessing()
‘ 重い計算処理のダミー(文字列結合ループなど)
Dim dummyText As String = “”
For i As Integer = 0 To 50000
dummyText &= “a”
Next
End Sub
End Module
【コードの解説と重要なメソッド】
1. `sw.Start()`
ストップウォッチのボタンを押す動作です。計測を開始(または再開)します。
2. `sw.Stop()`
ストップウォッチを止めます。この時点のタイムが保持されます。
3. `sw.Restart()`
現場で最もよく使う便利なメソッドです。 カウンタを「0」にリセットした上で、同時にスタートを切ってくれます。(`sw.Reset()` して `sw.Start()` するのと同じ効果です)
4. `sw.ElapsedMilliseconds`
計測された時間を「ミリ秒(整数)」で返します。画面出力やログ出力に最も使いやすいプロパティです。
5. `sw.Elapsed`
`TimeSpan` 型として値を取得します。`TotalMilliseconds` を使えば、「12.345ミリ秒」といった少数点以下の精密な時間まで取得可能です。
—
4. 伝説のチーフアーキテクトが教える「プロの計測極意」
ここからは、プログラミング初学者から「一歩抜き出た中級エンジニア」になるための、極めて重要なプロの知見(アーキテクチャの知識)をお伝えします。
計測コードを書いた際、「なぜか1回目の実行だけ極端に遅い…」という現象に遭遇することがあります。これには明確な理由があります。
① JITコンパイル(Warm-up)の影響
VB.NETコードは、実行時に「JIT(Just-In-Time)コンパイラ」によってCPUが直接理解できる機械語(ネイティブコード)に変換されます。
メソッドが最初に呼び出された時だけコンパイルのオーバーヘッドが発生するため、正確な速度を測りたい場合は、本計測の前に「ダミーで1回実行しておく(ウォームアップ)」のがプロの鉄則です。
② GC(ガーベジコレクタ)の割り込み
.NETはメモリ管理を自動で行ってくれますが、計測の最中にメモリのゴミ拾い(GC)が動くと、一時的にプログラムが停止し、計測値が大きくブレます。
「本気でマイクロ秒単位のアルゴリズム勝負をする」ような場合は、メモリ割り当て(`New`)をループ内で行わない実装にしてから計測する必要があります。
③ アロケーションフリーな極限の計測手法
もしインスタンス(`New Stopwatch()`)を作成するわずかなコストすら削りたい究極のパフォーマンス測定を行いたい場合は、`Stopwatch`の静的メソッド(`GetTimestamp`)を使用する手法もあります。
‘ 【上級者向け】Stopwatchのインスタンスすら作らない超高精度計測
Dim startTimestamp As Long = Stopwatch.GetTimestamp()
‘ 〜 超高速なミリ秒未満の処理 〜
Dim endTimestamp As Long = Stopwatch.GetTimestamp()
‘ 経過時間を計算(Ticksから TimeSpan への変換)
Dim elapsedSeconds As Double = (endTimestamp – startTimestamp) / Double(Stopwatch.Frequency)
Dim elapsedMilliseconds As Double = elapsedSeconds 1000
Console.WriteLine($”超高精度計測結果: {elapsedMilliseconds:F4} ms”)
—
5. 陥りやすいエラーと注意点
最後に、初心者がハマりがちなポイントを整理しておきましょう。
| 陥りやすいミス | 原因と対策 |
| :— | :— |
| `Stop()` し忘れて値を取り出す | `Stop()` を呼ばずに `Elapsed` を参照しても現在の経過時間は取れますが、後ろの処理が走ることで数値が変動し続けます。「測る箇所の直後で必ず Stop()」 を徹底しましょう。 |
| `Reset()` せずに使い回す | `Start()` ➔ `Stop()` ➔ `Start()` と呼ぶと、時間は「合算(加算)」されていきます。新しい区間を測りたい時は必ず `Restart()` を使いましょう。 |
| リリースビルド(Releaseモード)で測っていない | Visual Studioの「Debugモード」は、デバッグ用の余計なコードが含まれており低速です。本当の速度を測る時は、ビルド構成を 「Release」 に切り替えて計測してください。 |
—
まとめ
業務アプリの高速化は、野生の勘で行うものではありません。
「ボトルネックの正確な可視化」こそが、エンジニアリングの第一歩です。
1. 時間計測に `DateTime.Now` は使わない(精度不足とNTP問題のため)
2. 精密測定には `System.Diagnostics.Stopwatch` を使用する
3. 区間ごとに `sw.Restart()` を活用し、ボトルネックの箇所を特定する
この3つのポイントを押さえておけば、Visual Basic (VB / VB.NET)におけるパフォーマンス計測の基礎は完璧です!
コードのどこが遅いのかが目に見えて分かるようになると、プログラミングは一気にパズル感覚になって面白くなります。ぜひご自身のコードに `Stopwatch` を組み込んで、爽快なスピード改善を体験してみてくださいね。応援しています!
