2024/03/02

日時や時間の取得




プログラムでは日時データや経過時間を使う場面が多くあります。例えば、日時をファイル名として付けたり、一定時間待機したりする場合です。その日時値の取得や処理には Now関数やTimeValue関数が良く使われますが、それ以外にも色々なものが存在し、それぞれに特徴があります。
今回は、以下のような「時間を取得する関数など」について紹介します。
 ・Now関数・・・現在の日時データを取得。
 ・Timer関数・・・ミリ秒の取得可。午前0時からの値となる。
 ・GetLocalTime関数・・・Win32APIの関数で、ローカル日時をミリ秒で取得可。同類にGetSystemTime関数有り。
 ・SWbemLocatorオブジェクト・・・WbemScriptingライブラリで、文字列型の日時データを取得。
 ・timeGetTime関数・・・Win32APIの関数で、起動後の時間をミリ秒で取得可。
 ・timeGetSystemTime関数・・・Win32APIの関数で、起動後の時間をミリ秒で取得可。
 ・GetTickCount関数・・・Win32APIの関数で、起動後の時間をミリ秒で取得可。
 ・QueryPerformanceCounter関数・・・Win32APIの関数で、マイクロ秒単位で増えるカウンタ値。
また取得分解能の向上や、時間間隔を指定する際に使用する以下の関数についても紹介します。
 ・timeBeginPeriod関数・・・タイマー精度を変更する事で取得分解能向上が可。timeEndPeriod関数で元に戻す。
 ・TimeValue関数・・・文字列の時刻を1未満の小数データに変換。日付の場合はDateValue関数を使用。
なお「サンプルファイル」では、各コードをシート上のボタンから実行できるようにしています(図01)。
サンプルファイル上のボタン
図01

但し、timeBeginPeriod関数でタイマー精度を変更するコード(図32)はボタンへのリンク付けをしていません。これは途中でプログラムが止まった場合、動かしているパソコンに悪影響を与える可能性がある為です。御了承下さい。

1.Now関数

Now関数は、VBA関数と同時にワークシート関数にも存在します。スペルは一緒ですが別物です。

1-1.VBAの Now() での日時データ取得

VBAのNow関数は「現在の日時」のデータを取得するものです。戻り値はDate型で、例えば「2024年2月1日 18時」にNow関数を実行したとすると、得られる値は「2024/02/01 18:00:00」となります。
Nowに似た関数として「Date関数」と「Time関数」があります。データ型は両方ともNow関数と同じDate型で、「Now関数の日付部分」がDate関数、「Now関数の時刻部分」がTime関数という関係になります。
Date型はDouble型(倍精度浮動小数点型)と同等の8バイトで、先ほどのDate型の「2024/02/01 18:00:00」をDouble型の数値で表すと「45323.75」となります(いわゆるシリアル値)。日付値は「1日 = 数値の1」というルールですので、日付部分の「2024/02/01」が「45323」となり、また時刻部分の「18:00:00」は1日の3/4に当たりますので「0.75」となります。
取得するコード例を表すと図02のようになります。
  1. '========== ⇩(1) Now関数での日時値取得 ============
  2. Sub Get_Now()
  3.  Dim dateT As Date
  4.  Dim doubleT As Double
  5.  dateT = Now()
  6.  doubleT = Now()
  7.  MsgBox dateT & vbNewLine & doubleT
  8. End Sub
図02

02行目「Dim dateT As Date」では、変数をDate型で宣言し、03行目「Dim doubleT As Double」では変数をDouble型で宣言しています。
05行目「dateT = Now()」06行目「doubleT = Now()」は、どちらもNow関数を使って現在の日時を取得していますが、05行目ではDate型の変数に、06行目ではDouble型の変数に代入をしています。
08行目「MsgBox dateT & vbNewLine & doubleT」では、その両方の値を表示しています。出力された例が図03です。
データ型違いでのNow関数の値
図03

Double型では、時刻部分(小数点以下)が切りの良い値になることは稀で、図03のように桁数の多い値になります。
ちなみに図03の「10:01:01」は秒数で表すと「10 × 60 × 60 + 01 × 60 + 01 = 36,061」で、1日は「24 × 60 × 60 = 86,400」なので、「36,061 ÷ 86,400 = 0.4173726851851852・・・」と図03のような値となります(途中の桁で丸められます)。
このNow関数はDate型(例:2024/02/01 18:00:00)ですので、分解能は「秒」単位です。これを確認したのが図04です。確認方法としては、For~Next内でNow関数を連続して実行し、その値の頻度をグラフ化しています。
(なおFor~Nextは高速な制御ですし、Now関数の実行にもほとんど時間は掛からないので、適当に負荷を与えながら繰り返しています。)
Now関数の分解能
図04

図04の頻度グラフでは、26.0秒から38.0秒の間に11箇所のデータが出現(=隙間は12箇所)していますので、データの分解能は「1秒」となります。Now関数で得られるデータはDate型(秒単位)ですので、これが裏付けられた事になるかと思います。

1-2.Evaluate("Now()") でのミリ秒データ取得

上記のVBAのNow関数以外に、ワークシート関数にもNOW関数が存在します。そちらはどの位の分解能なのかを調べるために、まずセルにNOW関数を入力し、秒単位の値を表示させたのが図05です。
(細工をしていく内にNOW関数の値が更新されてしまうので、値の一貫性がとれていません。)
ワークシート上のNow関数の分解能
図05

つまり「VBAでのNow関数」での分解能は1秒でしたが、「ワークシート関数のNow関数」はミリ秒単位の分解能が得られる事が分かります。試しにVBA側から「Range("A1") = Now()」のようにVBAのNow関数値をワークシートに貼り付けても、小数点以下の値を得ることはできませんので、セル上で処理するからでは無く、ワークシート関数のNOW関数が「ミリ秒の分解能」を持っているようです。
このワークシート関数をVBA内で実行するのが、Evaluateメソッドです。
通常ワークシート関数をVBA側で実行するには「Application.WorksheetFunction」が思い浮かぶと思いますが、WorksheetFunctionオブジェクトは直接ワークシート関数を実行するものでは無く、単に「ワークシート関数に似たメソッド」を実行しているだけです。実際、WorksheetFunctionオブジェクト内には「Nowメソッド」は存在していません。
そこで「 Evaluate("Now()") 」のようにEvaluateメソッドを使う事で「ワークシート関数のNOW関数」が実行でき、図05の一番右図のような高い分解能の戻り値が得られる事になります。
なお、Evaluateの代わりに角カッコを使い「 [Now()] 」としてもOKです。
コードで表すと図06のようになります。
  1. '========== ⇩(2) EvalueateでのNow関数の値 ============
  2. Sub Get_EvaluateNow()
  3.  Dim dateT As Date
  4.  Dim doubleT1 As Double
  5.  Dim doubleT2 As Double
  6.  Dim doubleT3 As Double
  7.  dateT = Now()
  8.  doubleT1 = CDbl(dateT)
  9.  doubleT2 = Evaluate("Now()")
  10.  doubleT3 = [Now()]
  11.  MsgBox dateT & vbNewLine & doubleT1 & vbNewLine & doubleT2 & vbNewLine & doubleT3
  12. End Sub
図06

27行目「dateT = Now()」では、比較のためにVBAのNow関数値をDate型で受け取り、28行目「doubleT1 = CDbl(dateT)」ではDouble型に変換しています。28行目は「doubleT1 = dateT」と、自動的に変数doubleT1のデータ型(Double型)に変換されるようにしても問題ありません。
一方29行目「doubleT2 = Evaluate("Now()")」ではEvaluateメソッドを通し、また30行目「doubleT3 = [Now()]」ではEvaluateメソッドの代用の角カッコでワークシート関数のNOW関数値を取得します。
32行目「MsgBox dateT & vbNewLine & doubleT1 & vbNewLine & doubleT2 & vbNewLine & doubleT3」では、その4つの値を行を分けて表示しています。出力例が図07です。
Evaluateを通したNow関数の値
図07

時刻部分(=小数点以下)に注目すると、実行した時刻は「17:07:52」でDouble型(2行目)では「0.7137962963」です。ちなみにその1秒後「17:07:53」は「0.7138078703」という計算になります。
一方Evaluate("Now()")での値(小数点以下)は、「0.7138054398」で、52秒と53秒の間を指していることになります。つまり「1秒未満の分解能を持っている」事を意味しています。
では、この「Evaluate("Now()") 」又は「[Now()]」の分解能の確認をするため、VBAのNow関数の時と同様の方法で得た頻度グラフが図08です。小数点以下3桁(ミリ秒単位)でFrequency関数で頻度計算をしています。
Evaluate(Now())の分解能
図08

図08では、0.000秒から0.500秒の間を32分割する形でデータが出現しています(2か所に分かれてしまったデータもありますが、発生個数から考えて1か所のデータと見ています)。ですので、分解能は約15.6ms(0.0156秒)となります。
様々なサイトでは「Evaluate("Now()") の分解能はミリ秒」と説明し、あたかも0.001秒の分解能があるようにも聞こえますが、実際には約15~16ミリ秒のようです。
寄り道(Evaluateと角カッコの違い)
Evaluate("Now()")でも[Now()]でもミリ秒レベルの日時が取得できますが、Evaluateの方は引数内を「"(ダブルコーテーション)で囲む」必要があり、角カッコの方は「そのまま」の関数を指定します。
Evaluateメソッドの機能は、引数内の「関数や数式の文字列」を実行し、その結果を戻す というものです(引数内では変数が使用でき、変数を実値に置き換えたものを実行する事になります)。
今回のEvaluate("Now()")の場合は、「Now()」という文字列(「"(ダブルコーテーション)」で囲んで "Now()" となる)を引数に指定することでワークシートのNOW()関数が実行されます。
このNow()を文字列変数に一旦格納し、それをEvaluateメソッドに渡す時には、以下のように変数として「"(ダブルコーテーション)」を付けずに指定します。この場合MsgBoxではDouble型の日付+時刻値が表示されます。
  • Dim str As String
  •  
  • str = "Now()"
  • MsgBox Evaluate(str)
図09

一方で角カッコの方は「角カッコ内は文字列」とみなし「その文字列を実行」します。ですので図09と同様なコードにして図10としてしまうと、「strという変数に格納されている値」を戻すので、「"Now()"」という文字列が得られる事になります。
  • Dim str As String
  •  
  • str = "Now()"   '←変数strには"Now()"の文字列が入る
  • MsgBox [str]
図10

かと言って「str = Now()」としてしまうと、変数strには「日付+時刻」の文字列が格納されるので、MsgBoxでも日付+時刻が表示されます。
  • Dim str As String
  •  
  • str = Now()   '←変数strには「日付+時刻」が文字列として入る
  • MsgBox [str]
図11

よってEvaluateメソッドの引数に「変数を使って指定」したい場合は、角カッコでは無くEvaluateを使う必要があります。

寄り道(引数を表す丸カッコ)
VBAのNow関数を使う場合、引数を表す丸カッコ有り( Now() )でも、無し( Now )でも、どちらでも値は取得できます。ではEvaluateメソッドや角カッコでNow関数を囲んだときの出力結果は以下のようになります。
 ・Evaluate("Now()") ←Double型データ
 ・Evaluate("Now")  ←エラー 2029(名前が違うというエラー内容)
 ・[Now()]   ←Double型データ
 ・[Now]    ←Date型データ(普通の秒単位の日時データ)
上記のように、正しいのは「丸カッコをつけるNow」で、「 Evaluate("Now") 」と丸カッコを付けないとエラーとなります。これはセル内で「=Now」のように丸カッコを付けずに入力してしまうとエラーとなるのと同じ現象です。
正確に言うと、Evaluateメソッドは引数に指定した「"式"を評価」して結果を返す関数であるため、丸カッコの無い「Now」を式では無く関数名か何かと判断してしまうためのようです(セル上でも一緒)。
しかしEvaluateの代用である角カッコでの挙動は少し違います。
丸カッコ付きは正しい値が出るのは当然ですが、丸カッコを付けずに「 [Now] 」のようにすると、Date型(秒単位)のデータとして出力されるのです。現象から推測すると「丸カッコを付けないとEvaluateの引数では無く、単なるVBAの「Now」と認識」しているかのようですが、ちょっと納得できません。
なおNowと同様に、現在の日付・時刻・時刻のミリ秒を得る関数である「Date()」「Time()」「Timer()」などをEvaluateメソッドに指定してもエラーが出ます。これはEvaluateメソッドが「VBAの中でワークシート関数を実行させるもの」である為で、Date・Time・Timerは「VBAの関数」だからです。またワークシート関数のDATEやTIMEは、引数で指定した値を日付や時刻に変換する関数で「引数が必須」である事も関係します。またTimerについてはワークシート関数には存在もしていません。
Now関数は運よくVBA側にもワークシート側にも関数として存在しますのでこのような誤解が生じますが、図05のところで説明した通り、関数名は同じでも「分解能が異なる別の関数」ということになります。
なおVBAのDate関数の代わりにワークシート関数のTODAY関数を使い「Evaluate("Today()")」とすると、今日の日付の「シリアル値」が得られます。

2.Timer関数

Timer関数は「その日の午前0時からの経過秒」を取得するものです。得られる戻り値はSingle型です。コード例は図12のようになります。
  1. '========== ⇩(3) Timerの値 ============
  2. Sub Get_Timer()
  3.  Dim dateT As Date
  4.  Dim doubleT As Double
  5.  Dim singleT As Single
  6.  dateT = Time()
  7.  doubleT = Time() * 60 * 60 * 24
  8.  singleT = Timer()
  9.  MsgBox dateT & vbNewLine & doubleT & vbNewLine & singleT
  10. End Sub
図12

46行目「dateT = Time()」では、現在の時刻部分をDate型で取得し、47行目「doubleT = Time() * 60 * 60 * 24」ではその時刻を秒単位で取得しています。48行目「singleT = Timer()」ではTimer関数で秒数を取得します。
50行目「MsgBox dateT & vbNewLine & doubleT & vbNewLine & singleT」では、それらを表示しています。出力の例が図13になります。
Timer関数の時刻毎の出力値
図13

図13の各図は、図12のコードを様々な時刻に実施したもので、時刻順に並べています。1行目のデータが時刻、2行目はその時刻をシリアル値の秒数にしたもの。3行目のデータがTimer関数の戻り値になるのですが、早い時間帯では「小数点3桁」が得られ、遅い時間帯(右から2番目以降)になると「小数点2桁」になるのが分かります。
Timer関数は「Single型」の値を戻すのですが、Single型は「有効桁数=7桁」なので、図13の右から3番目の0:16:42のデータでは「1002.283」と「整数部(秒以上)4桁+小数点以下(秒未満)3桁=合計7桁」となります。一方 2:46:44のデータ(図13の右から2番目)では「10004.18」と「整数部(秒以上)5桁+小数点以下(秒未満)2桁=合計7桁」と、合計桁数(7桁)は決まっているため、整数部の桁が増えると小数点以下の桁数が減ってしまうという関係になるようです。
なお図13の左側3つは全て小数点以下3桁となっています。Single型の桁数が全て出ていない事になるのですが、これはTimer関数の元データが小数点以下3桁(ミリ秒)の有効数字しか持っていないという意味なのかもしれません。
このTimer関数での分解能を、Now関数と同様の方法で確認したのが図14です。なお使ったデータは小数点以下2桁で、Single型の値を目で読み取った場合を想定しています。
Timer関数の分解能(小数点以下2桁)
図14

図14は値出現の間隔がバラバラというイメージですが、分解能は「約15ミリ秒」と分かります。またこのバラバラ感は、10ミリ秒単位(小数点2桁)のデータに対し約15ミリ秒の分解能となっているためと思われます。
なおSingle型は表示上は有効7桁ですが、内部的には9桁(?)を持っているらしく、Single型データをセル上に出力したりすれば、もう少し分解能の高いデータが取得できます。その少し分解能の高い(小数点以下3桁)データで作成した頻度グラフが図15です。
Timer関数の分解能(小数点以下3桁)
図15

図14に対して図15は、値出現の間隔がほぼ均等になってきています。ただし分解能的に見れば、相変わらず約15ミリ秒です。
なお、図13で説明した「小数点以下3桁が取得できる時間帯(~02:46 頃)」のデータでも、分解能約15ミリ秒は変わりませんでした。
Timer関数は時刻を得るというよりは、開始と完了の値から掛かった時間を測ることに良く使われると思います。しかし、日付(≒午前0時)をまたぐ時に使用するとマイナス値になってしまったり、停止時間の基準にTimer関数を使用してしまうと丸1日停止してしまう事があるので注意が必要です。

3.GetLocalTime関数

GetLocalTime関数はWin32APIの関数の1つで、現在の日時をミリ秒単位で得るものです。なおGetLocalTime関数は、値をSYSTEMTIME構造体として戻してきますので、SYSTEMTIME構造体も一緒に定義する必要があります。
  1. '========== ⇩(4) Win32APIの宣言 ============
  2. #If Win64 Then
  3.  Declare PtrSafe Sub GetLocalTime Lib "kernel32" (lpSystemTime As SYSTEMTIME)
  4. #Else
  5.  Declare Sub GetLocalTime Lib "kernel32" (lpSystemTime As SYSTEMTIME)
  6. #End If
  7. '========== ⇩(5) SYSTEMTIME構造体の定義 ============
  8. Type SYSTEMTIME
  9.  wYear As Integer      '←年
  10.  wMonth As Integer     '←月
  11.  wDayOfWeek As Integer   '←曜日(0=日曜、1=月曜、・・・)
  12.  wDay As Integer       '←日
  13.  wHour As Integer      '←時
  14.  wMinute As Integer     '←分
  15.  wSecond As Integer     '←秒
  16.  wMilliseconds As Integer   '←ミリ秒
  17. End Type
  18. '========== ⇩(6) GetLocalTimeの値 ============
  19. Sub Get_GetLocalTime()
  20.  Dim sysT As SYSTEMTIME   '←データを格納する変数
  21.  Dim strT As String      '←出力文字列
  22.  Call GetLocalTime(sysT)
  23.  strT = sysT.wHour & ":" & sysT.wMinute & ":" & sysT.wSecond & "." & sysT.wMilliseconds
  24.  MsgBox Time() & vbNewLine & strT
  25. End Sub
図16

61~65行目はWin32API関数の宣言です。
Excelが64ビット版の時は62行目「Declare PtrSafe Sub GetLocalTime Lib "kernel32" (lpSystemTime As SYSTEMTIME)」が、32ビット版の時は64行目「Declare Sub GetLocalTime Lib "kernel32" (lpSystemTime As SYSTEMTIME)」が使用されます。違いは、64ビット版の方にのみ「Subの前に PtrSafeが付く」ところです。
68~77行目は、SYSTEMTIME構造体の定義で、年からミリ秒までが並んでいます。なお、使わないからと項目を省いたり順番を変えたりすると、間違った場所に値が入ってしまい誤った出力値となりますので、ご注意下さい。
80~88行目が実行するプロシージャです。
まず81行目「Dim sysT As SYSTEMTIME」は、GetLocalTime関数の戻り値を格納する変数の宣言ですが、戻り値は「SYSTEMTIME構造体」ですので、それを受け取る変数(sysT)のデータ型も「As SYSTEMTIME」とします。
84行目「Call GetLocalTime(sysT)」でGetLocalTime関数を呼び出し、戻り値を変数(ここではsysT)に代入します。
85行目「strT = sysT.wHour & ":" & sysT.wMinute & ":" & sysT.wSecond & "." & sysT.wMilliseconds」では、戻り値の構造体データの中から、必要なデータを並べて出力用の文字列を作成しています。ここでは、時・分・秒・ミリ秒 を使っています。
87行目「MsgBox Time() & vbNewLine & strT」で、時刻データをメッセージとして出力します。なお比較の為上段に、VBAのTime関数で時刻を追加しています。出力された様子が図17です。
GetLocalTime関数の出力の様子
図17

GetLocalTime関数が戻すSYSTEMTIME構造体には「wMilliseconds」というミリ秒の要素がありますので、当然ミリ秒(小数点3桁)表示が可能です。
また図17の分表示の部分でも分かるように、GetLocalTime関数で得られる値は数値(Integer型)なので「1桁の場合は1桁出力」です。例えば「5分」の時には85行目内の「sysT.wMinute」は「数値の5」となります(時刻表示用に「05」となってはくれません)。ですので、値を整列させたい場合は「Format(sysT.wMinute, "00")」等のように細工をする必要があります。
GetLocalTime関数ではミリ秒単位のデータが得られますが、その分解能を調べたのが図18です。方法はNow関数他と同じです。
GetLocalTime関数の分解能
図18

構造体の「wMilliseconds」にはキチンと小数点以下3桁の値が入りますが、図18で分かるように「分解能は約15ミリ秒」です。
寄り道(GetSystemTime関数)
GetLocalTime関数と似たAPI関数に「GetSystemTime関数」があります。使い方等はGetLocalTime関数とほぼ一緒ですが、取得する時刻がUTC(Universal time coordinated:協定世界時)となるため、日本時間の9時間前の日時になります。
なお宣言としては「Declare Sub GetSystemTime Lib "kernel32" (lpSystemTime As SYSTEMTIME)」とします。
SYSTEMTIME構造体の定義は、GetLocalTime関数と同じです。

4.SWbemLocatorオブジェクト

WbemScriptingライブラリのSWbemLocatorオブジェクトを使うことで文字列型の日時データを取得できます。このデータはマイクロ秒(小数点6桁)の分解能枠を持っていますが、データとして得られるのは小数点3桁(ミリ秒)までのようです(残りはゼロで埋まっている)。
  1. '========== ⇩(7) LocalDateTimeの値 ============
  2. Sub Get_SWbemLocator()
  3.  Const WQL = "Select LocalDateTime from Win32_OperatingSystem"   '←日時データを取り出すSQL文
  4.  Dim wmLocator As Object   '←SWbemLocatorオブジェクト
  5.  Dim strT As String      '←LocalDateTime値(日時+オフセット時間)
  6.  Dim msgT As String     '←表示させる日時データ
  7.  Set wmLocator = CreateObject("WbemScripting.SWbemLocator")
  8.  strT = wmLocator.ConnectServer().ExecQuery(WQL).ItemIndex(0).Properties_.Item("LocalDateTime").Value
  9.  msgT = Mid(strT, 1, 4) & "/" & Mid(strT, 5, 2) & "/" & Mid(strT, 7, 2) & " " & _
  10.      Mid(strT, 9, 2) & ":" & Mid(strT, 11, 2) & ":" & Mid(strT, 13, 6)
  11.  MsgBox Now() & vbNewLine & msgT
  12.  Set wmLocator = Nothing
  13. End Sub
図19

102行目「Const WQL = "Select LocalDateTime from Win32_OperatingSystem"」は、自コンピュータから日時データを取り出すためのSQL文です。
107行目「Set wmDate = CreateObject("WbemScripting.SWbemLocator")」では、WbemScriptingライブラリのSWbemLocatorオブジェクトを生成しています。
109行目「strT = wmLocator.ConnectServer().ExecQuery(WQL).ItemIndex(0).Properties_.Item("LocalDateTime").Value」では、日時データを変数strTに文字列として代入します。
右辺の「wmLocator」は107行目で生成したSWbemLocatorオブジェクト、「ConnectServer()」は引数を指定していない(丸カッコ内がカラ)ため、ローカルコンピュータに接続をします。接続後は「ExecQuery(WQL)」で、102行目に定数宣言してあるSQL文を実行します。実行した結果は「ItemIndex(0).Properties_.Item("LocalDateTime")」に書き込まれますので、その値(Value)を取得します。
この値は、例えば「"20240229161143.337000+540"」のように「yyyymmddhhnnss.ffffff+nnn」の形になっています。先頭から年(yyyy)+ 月(mm)+ 日(dd)+ 時(hh)+ 分(nn)+ 秒(ss)+ ピリオド + ミリ秒(ffffff)が連続した文字列です。最後に付いている「+nnn」は、世界標準時間(GMT/UTC)とローカル時間のずれ(分単位)で、日本時間の場合は「+540」となります。
取得したデータが文字列ですので、それぞれを切り出して表示データにする必要があります。111~112行目「msgT = Mid(strT, 1, 4) & "/" & Mid(strT, 5, 2) & "/" & Mid(strT, 7, 2) & " " & Mid(strT, 9, 2) & ":" & Mid(strT, 11, 2) & ":" & Mid(strT, 13, 6)」では、「年/月/日 時:分:秒.ミリ秒」となるように加工しています。
なお、秒の値は「Mid(strT, 13, 6)」とすることで「小数点3桁」までが表示されることになります。データ枠としてはマイクロ秒(小数点6桁)までが用意されているので「Mid(strT, 13, 9)」としても良いのですが、私のPCで確認してみるとマイクロ秒部分の小数点4~6桁目は全てゼロであったためミリ秒までの表示としてあります。但し、マイクロ秒部分にもデータが入るような環境も存在するかもしれません。
その加工した文字列を114行目「MsgBox Now() & vbNewLine & msgT」で出力しています。比較としてNow関数での日時データも一緒に出力しています。出力結果は図20のようになります。
SWbemLocatorオブジェクトを使った表示結果
図20

なお、ここでは実行時バインディング(図19の107行目のように、CreateObjectを使ってオブジェクト生成)をしていますが、図21のようにVBEの「ツール」→「参照設定」から「Microsoft WMI Scripting V1.2 Library」を参照可能な状態(レ点を付ける)にすることで、事前バインディングが可能となります。
WMI Scriptingライブラリの参照設定
図21

データの分解能ですが、SWbemLocatorオブジェクトで取得する値は小数点6桁のマイクロ秒の分解能枠を持っていますが、値が入っているのは3桁(ミリ秒)までのようです。Now関数等と同様の方法で分解能を調べたのが図22です。
SWbemLocatorオブジェクトのデータの分解能
図22

分解能の前に、SWbemLocatorオブジェクトでの日時データを取得するのに必要な時間(図19の109行目の実行時間)を調べてみると、私のノートPCで約12ミリ秒でした。「まとめ」の所でも紹介しますが、この12ミリ秒という実行時間は他の関数等に比べると非常に遅いことが分かります。その状態で測定した結果が図22ですので正確な事は言えませんが、見掛けの分解能は約15ミリ秒のようです。
寄り道(SWbemDateTimeオブジェクト)
なお、同じWbemScriptingライブラリ内の「SWbemDateTimeオブジェクト」を使うことでもSWbemLocatorオブジェクトと同様のデータ(小数点6桁のマイクロ秒)を取得できます。
しかしオブジェクトが生成された時点では日時データは空なので、SetVarDateメソッドで初期値を指定する必要があります。今回の「現在の日時データ等を取得」のテーマとは違う気がしましたので割愛しました。

5.timeGetTime関数

timeGetTime関数はWin32APIの関数の1つで、「Windowsが起動してからのミリ秒」を整数値で戻します。
そのため上記までのような「日時の絶対値」を取得するのでは無く、差を測って「時間の間隔」の取得に使われる事が多いと思われます。なお戻り値は32ビット(=4バイト)の符号なし整数で、最大値に達したらゼロに折り返されるようです。
APIですので、宣言部で「Private Declare Function timeGetTime Lib "winmm.dll" () As Long」の宣言が必要です。
戻り値が「符号なしの32ビット整数(0~4,294,967,296)」なので、起動後49.7日間は線形にデータが得られますが、その受取先を「Long型(符号付き32ビット:−2,147,483,648 ~ +2,147,483,647)」にしてしまうと、最大の半分の24.8日間でオーバーフローし、エラーが出てしまう事になります。
ですので対応としては、64ビット版のExcelであれば LongLong型(64ビット)で受け取ったり、32ビット版でもDouble型(64ビット)で受け取ったりする事で、「正側の最大値 > timeGetTimeの最大値」となり大丈夫そうです。
コード例を示すと図23のようになります。
  1. '========== ⇩(8) Win32APIの宣言 ============
  2. #If Win64 Then
  3.  Declare PtrSafe Function timeGetTime Lib "winmm.dll" () As Long
  4. #Else
  5.  Declare Function timeGetTime Lib "winmm.dll" () As Long
  6. #End If
  7. '========== ⇩(9) timeGetTimeの値 ============
  8. Sub Get_timeGetTime()
  9.  Dim doubleT As Double
  10.  doubleT = timeGetTime / 1000
  11.  MsgBox doubleT
  12. End Sub
図23

131~135行目では、Win32APIのtimeGetTime関数の宣言をしています。
140行目「doubleT = timeGetTime / 1000」では、ミリ秒単位の整数で取得した稼働時間を1000で割り、秒単位(小数点3桁)の値を変数doubleTに代入しています。
141行目「MsgBox doubleT」では、140行目で計算した値を表示させています。得られる表示は図24のようになります。
timeGetTime関数での値表示
図24

戻ってくる値はミリ秒単位(小数点以下3桁)の値なのですが、分解能を調べてみると図25のように「約15ミリ秒」との計測結果となりました。
timeGetTime関数の分解能
図25

6.timeGetSystemTime関数

timeGetTimeと似た関数として「timeGetSystemTime関数」があります。これもシステム起動からのミリ秒を整数値で戻すものですが、戻り値がMMTIME構造体となることがtimeGetTimeと異なる点です。
この関数について色々と調べてみたのですが、VB関連で記述したサイトは見つからず、とりあえずコードを書いてみたのが以下です。
  1. '========== ⇩(10) Win32APIの宣言 ============
  2. #If Win64 Then
  3.  Declare PtrSafe Function timeGetSystemTime Lib "winmm" (pmmt As MMTIME, ByVal cbmmt As Long) As Long
  4. #Else
  5.  Declare Function timeGetSystemTime Lib "winmm" (pmmt As MMTIME, ByVal cbmmt As Long) As Long
  6. #End If
  7. '========== ⇩(11) MMTIME構造体 ============
  8. Type MMTIME
  9.  wType As Long   '←タイプ?
  10.  u As Long   '←日時データが入る
  11. End Type
  12. '========== ⇩(12) timeGetSystemTimeの値 ============
  13. Sub Get_timeGetSystemTime()
  14.  Dim Mtime As MMTIME
  15.  Call timeGetSystemTime(Mtime, 16)
  16.  MsgBox Mtime.u / 1000
  17. End Sub
図26

151~155行目のWin32API関数宣言で、timeGetSystemTime関数には2つの引数を指定する必要があることが分かります。第1引数はMMTIME構造体(158~161行目で宣言する内容)で、ここで日時データを受け取ります。第2引数には取り出すデータサイズ(バイト単位)を指定します。
158~161行目はMMTIME構造体の宣言で、1つ目の要素(wType:Long型)がデータタイプを表し、2つ目の要素(u:Long型)が「起動後のミリ秒数」を表すようです。
実行プロシージャが164~170行目です。165行目「Dim Mtime As MMTIME」は、timeGetSystemTime関数の第1引数の変数をMMTIME構造体のデータ型として宣言します。
167行目「Call timeGetSystemTime(Mtime, 16)」でAPI関数を呼び出しています。この時の第2引数は「取り出すデータサイズ」なので、Long型+Long型で8バイト( Len(Mtime) でも同じ)のはずなのですが、8(バイト)を指定してもデータ取得できません。なぜか倍の「16バイト」を指定すると、ミリ秒値が取得できます。
この理由は、MMTIME構造体が間違っている(あと2つLong型が存在する)くらいしか思いつかないのですが、情報が無く良く分かりません。
とにかくデータがMMTIME構造体として得られたら、169行目「MsgBox Mtime.u / 1000」では、戻り値(MMTIME構造体)の「要素u」に入ったミリ秒値を取り出し、秒単位にするために1,000で割ってメッセージ表示をしています。得られる表示は図27のようになります。
timeGetSystemTime関数での値表示
図27

なおtimeGetSystemTimeの最大値は分かりません。しかしtimeGetTimeとほぼ同一なものと説明されている事から、timeGetTimeと同じく「符号なしの32ビット?」と推定されます(49.7日が最大)。
このtimeGetSystemTimeの分解能ですが、以下の様にtimeGetTimeの分解能と同じく「約15ミリ秒」です。
timeGetSystemTimeの分解能
図28

7.GetTickCount関数

timeGetTime関数と似たようなAPI関数として「GetTickCount関数」があります。これも「Windows起動からの経過時間」をミリ秒単位の整数値で返すものです。
なお、GetTickCount関数を使用する際の宣言は「Declare Function GetTickCount Lib “kernel32” () As Long」となります。
コード例を示すと、以下の様になります。
  1. '========== ⇩(13) Win32APIの宣言 ============
  2. #If Win64 Then
  3.  Declare PtrSafe Function GetTickCount Lib “kernel32” () As Long
  4. #Else
  5.  Declare Function GetTickCount Lib “kernel32” () As Long
  6. #End If
  7. '========== ⇩(14) GetTickCountの値 ============
  8. Sub Get_GetTickCount()
  9.  Dim doubleT As Double
  10.  doubleT = GetTickCount / 1000
  11.  MsgBox doubleT
  12. End Sub
図29

190行目「doubleT = GetTickCount / 1000」ではGetTickCount関数を実行しています。起動からのミリ秒を整数値として取得し、1000で除算することで「秒単位」で表示をさせています。表示は図30のようになります。
GetTickCount関数での値表示
図30

分解能について調べたのが図31です。他サイトでは「分解能は環境や状況により 1~5ミリ秒と変わる」との情報もありますが、今回調べた結果では、timeGetTime関数と同等の「約15ミリ秒」のようです。
GetTickCount関数の分解能
図31

timeGetTimeとGetTickCountは良く似ています。同じWindowsのWin32APIの一部であり、両方ともシステムの経過時間を取得するために使用されます。
異なる点はGetTickCountが「timeBeginPeriodでのタイマー高精度化でも分解能が向上しない」事です。詳細は「タイマー精度変更による高分解能化」を参照下さい。
また「GetTickCountはスループットが良い(実行時間が短い)」との情報もあります。しかし今回測った結果(まとめを参照下さい)では、ほぼ同じでした。
寄り道(GetTickCount64関数)
GetTickCountと似たものに「GetTickCount64」があります。同様に「Windows起動からの経過時間」をミリ秒単位で返すものですが、「GetTickCount64」の方は戻り値が64ビット(8バイト)のため、起動から2.9億年までの期間が扱えます。
なお、API宣言は「Declare PtrSafe Function GetTickCount64 Lib “kernel32” () As LongLong」とします。LongLong型となっていることから分かるように、64ビット版のOfficeでしか使用できません。

8.タイマー精度変更(timeBeginPeriod関数)による高分解能化

上記で説明した関数等での分解能は、(VBAのNow関数を除き)どれもほぼ「15ミリ秒」です。実はこの15ミリ秒というのは「Windowsのデフォルトのタイマー最小精度≒15msec」という所から来ているようです。
(正確には、Windowsでは1秒間に64回のタイマー割り込み → 15.6ミリ秒 という計算だそうです。)
このタイマーは、Windows上で動く様々なアプリや、またアプリ内の動作の順番を切り替える間隔です。このタイマー精度に乗っかる形で各関数が日時や時間を返してくるので、どれも「約15ミリ秒」という分解能になるようです。
このタイマー精度は変更することが可能です。Win32APIのtimeBeginPeriod関数で変更し、timeEndPeriod関数で標準に戻します。それぞれの引数には、変更する「ミリ秒の値」を指定します。
例えばワークシートのNOW関数「 Evaluate("Now()") 」で日時データを得るコードに対し、timeBeginPeriodを使用して取得値の分解能を向上させるには、以下のようにします。
  1. '========== ⇩(15) Win32APIの宣言 ============
  2. #If Win64 Then
  3.  Declare PtrSafe Function timeBeginPeriod Lib "winmm.dll" (ByVal uPeriod As Long) As Long
  4.  Declare PtrSafe Function timeEndPeriod Lib "winmm.dll" (ByVal uPeriod As Long) As Long
  5. #Else
  6.  Declare Function timeBeginPeriod Lib "winmm.dll" (ByVal uPeriod As Long) As Long
  7.  Declare Function timeEndPeriod Lib "winmm.dll" (ByVal uPeriod As Long) As Long
  8. #End If
  9. '========== ⇩(16) Evaluate("Now()")の値 ============
  10. Sub Get_Now2()
  11.  Dim doubleT As Double
  12.  timeBeginPeriod 1
  13.   doubleT = Evaluate("Now()")
  14.  timeEndPeriod 1
  15.  MsgBox doubleT
  16. End Sub
図32

213行目「doubleT = Evaluate("Now()")」で日時データを取得している場合、その前後でタイマー精度を変更します。ここでは212行目「timeBeginPeriod 1」で、引数に1(ミリ秒)を指定することで「タイマー精度を1ミリ秒」に変更しています。
処理(ここでは213行目)が完了したら、214行目「timeEndPeriod 1」でタイマー精度を元(15ミリ秒)に戻します。
なおタイマー変更時(212行目のtimeBeginPeriod 1)と戻し時(214行目のtimeEndPeriod 1)の「変更値」は同じ値を指定する必要があります。
もしtimeEndPeriodの引数に「timeBeginPeriodの引数とは異なる値」を指定しても、タイマー精度は変更した時のままとなり、「最初のtimeBeginPeriodの引数値」でtimeEndPeriodを実行しない限り元に戻りません。
なおtimeBeginPeriod・timeEndPeriodの実行時、成功した場合は「0」が戻り、失敗は「97」が戻るようなので、この戻り値を使って変更を確実に戻すような別処理も考えられます。
しかし、元に戻さなかった時は当然ですが、元に戻せなかった(設定を間違えた、エラーでtimeEndPeriodが実行されなかった等)時にはタイマーが高精度で動きっぱなしとなる事もあります。
このタイマー精度向上による副作用としては以下のような事が考えられます。
タスクの頻繁な切り替えによる、システムの全体的なパフォーマンスが低下
プロセッサーの使用率が増加(10~25%)
消費電力が増える
バッテリー寿命に影響(10%低下?)

ですので「ハードへの悪影響+Windows本体を含めた他のアプリにも影響」を考え、この機能を安易に使用する事は控えた方が良いと思います。もし高い分解能が必要な時は「QueryPerformanceCounter関数」を使う方が安全と思います。
以下では、各関数等での「分解能の変化」を紹介します。各図の左側が標準のタイマー精度(15ミリ秒)、右側がタイマー精度を1ミリ秒に変更したものです。なお各関数の所で紹介した図と以下の左側図は基本的に同じはずですが、改めて頻度計測をしたため分解能も微妙に異なっている事があります。ご了承下さい。

8-1.タイマー高精度化による各関数等の分解能

8-1-1.Evaluate("Now()")

ワークシートのNOW関数(VBAでは、Evaluate("Now()"))での分解能を調べた結果が図33です。
タイマー精度を向上させた時のEvaluate_Nowの時間頻度
図33

ワークシートのNOW関数は、図05でも説明したように「小数点2桁」までが表示されます。タイマー精度が標準(15ミリ秒)の場合、図33の左側のように分解能は約15ミリ秒でしたが、タイマーを高精度(1ミリ秒)にすると図33の右側のように「分解能10ミリ秒」とすることが出来ます。小数点2桁の値なので「10ミリ秒が限界」と思われます。
なおVBAのNow関数は分解能が1秒(Date型)ですし、試しにtimeBeginPeriodでタイマーを高精度にしてみましたが変化ありませんでした。

8-1-2.Timer関数

Timer関数での分解能を調べた結果が図34です。
タイマー精度を向上させた時のTimer関数の時間頻度
図34

Timer関数の戻り値はSingle型(有効7桁)で小数点2桁のミリ秒が得られます。2桁ならばワークシート関数のNow()と同じ10ミリ秒になるはずですが、Single型は内部的にはもう少し有効桁数があるようで、図34の右側のように「約8ミリ秒」の分解能が得られています。
しかし、もし有効桁数が小数点3桁ならば、約1ミリ秒の分解能となっても良さそうに思えます。なぜ中途半端な8ミリ秒なのか少し不思議です。

8-1-3.GetLocalTime関数

GetLocalTime関数での分解能を調べた結果が図35です。
タイマー精度を向上させた時のGetLocalTime関数の時間頻度
図35

GetLocalTime関数は、SYSTEMTIME構造体で小数点3桁のミリ秒枠が確保(wMilliseconds)されています。タイマー精度をUPさせる事で、図35の右側のように分解能が「1ミリ秒」まで向上しています。

8-1-4.SWbemLocatorオブジェクト

SWbemLocatorオブジェクトのLocalDateTimeプロパティでの分解能を調べた結果が図36です。
タイマー精度を向上させた時のSWbemLocatorの時間頻度
図36

SWbemLocatorオブジェクトは実行そのものに約12ミリ秒の時間がかかる取得方法です。タイマーが標準状態(図36の左側)では、その実行のタイミングとタイマーのタイミングが近いからか、非常に不安定な出現の仕方をしています。
一方タイマー精度をUP(図36の右側)すると、安定した形になりますが、実行に時間が掛かるために分解能はそれほど向上せず、実行時間(約12ミリ秒)が限界のようです。

8-1-5.TimeGetTime関数

TimeGetTime関数での分解能を調べた結果が図37です。
タイマー精度を向上させた時のTimeGetTime関数の時間頻度
図37

TimeGetTime関数はミリ秒の整数値を戻してきます。タイマー精度をUP(図37の右側)させる事で、分解能が「1ミリ秒」まで向上します。

8-1-6.TimeGetSystemTime関数

TimeGetSystemTime関数での分解能を調べた結果が図38です。
タイマー精度を向上させた時のTimeGetSystemTime関数の時間頻度
図38

TimeGetSystemTime関数はミリ秒の値をMMTIME構造体に戻すものです。タイマー精度をUP(図38の右側)させる事で分解能は向上しますが、1ミリ秒までは上がらず「1.7ミリ秒」との結果になりました。しかも不安定な出現の仕方をしています。
他サイトでは「TimeGetTime関数とTimeGetSystemTime関数で異なるのは戻り値が構造体くらい」と紹介されています。確かにタイマーが標準であれば図37図38の左側のように、ほぼ同じように思えますが、タイマーを高精度にすると右図のように違いが出てきます。

8-1-7.GetTickCount関数

GetTickCount関数での分解能を調べた結果が図39です。
タイマー精度を向上させた時のGetTickCount関数の時間頻度
図39

GetTickCount関数はタイマー精度を変更させても「分解能は不変」です。多少右側のほうが安定して値が出現するかな?くらいです。
TimeGetTime関数とGetTickCount関数とは、ほぼ一緒の機能です。しかしタイマー精度変更に対しては、全く異なります。

9.QueryPerformanceCounter関数

「QueryPerformanceCounter関数」は、Windowsのタイマーのパフォーマンスカウンター(Windows起動からカウントを開始)の現在値を取得する関数です。一方、タイマーが秒当たり何カウントするか(周波数)は「QueryPerformanceFrequency関数」で得ることが出来ます。つまり「QueryPerformanceCounter値/QueryPerformanceFrequency値」の計算により、小数点以下を含めた「起動からの秒数」が得られます。
コード例を示すと、以下の様になります。
  1. '========== ⇩(17) Win32APIの宣言 ============
  2. #If Win64 Then
  3.  Declare PtrSafe Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
  4.  Declare PtrSafe Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
  5. #Else
  6.  Declare Function QueryPerformanceFrequency Lib "kernel32" (frequency As Currency) As Long
  7.  Declare Function QueryPerformanceCounter Lib "kernel32" (procTime As Currency) As Long
  8. #End If
  9. '========== ⇩(18) QueryPerformanceCounterの値 ============
  10. Sub Get_PerformanceCounter()
  11.  Dim Cnt As Currency   'カウント数(起動後~)
  12.  Dim Freq As Currency   '周波数(カウント数/秒)
  13.  Call QueryPerformanceFrequency(Freq)
  14.  Call QueryPerformanceCounter(Cnt)
  15.  MsgBox Freq * 10000 & vbNewLine & Cnt * 10000 & vbNewLine & Cnt / Freq
  16. End Sub
図40

231~237行目は、QueryPerformanceCounter関数(カウント数)とQueryPerformanceFrequency関数(周波数)のWin32API関数の宣言です。ここではCurrency型(8バイト固定小数点型)で戻り値を受け取っています。
この他に、LARGE_INTEGER構造体やDouble型で受け取る方法も考えられます。これについては下の「受け取るデータ型について」を参照下さい。
244行目「Call QueryPerformanceFrequency(Freq)」でQueryPerformanceFrequency関数を呼び出し、引数(ここでは変数Freq)に1秒あたりのカウント数(周波数)が入ります。
245行目「Call QueryPerformanceCounter(Cnt)」でQueryPerformanceCounter関数を呼び出し、引数(ここでは変数Cnt)に起動後のカウント値が入ります。
247行目「MsgBox Freq * 10000 & vbNewLine & Cnt * 10000 & vbNewLine & Cnt / Freq」では、カウント数を周波数で除算して「起動後の秒数」を出力しています。
比較のために、周波数とカウント数も併せて表示させていますが、図40ではCurrency型(小数点4桁)でデータを受け取っていますので、実際の値に戻すため、周波数とカウント数には10,000を乗算しています。
出力される表示は以下のようになります。
QueryPerformanceCounter関数での値表示
図41

周波数については機器や環境等により異なるようですが、私のノートPCでは「10,000,000(10MHz)」でした。ですので見かけの分解能は0.1マイクロ秒(1/10,000,000秒)となります。
(なお図40では受け取りデータ型をCurrency(小数点4桁の固定小数点型)としているため、変数Freqの値は10,000で割った「1,000」という値になっています。変数Cntも10,000で割った値です。)
この関数の分解能を確認したのが図42の左側です。
QueryPerformanceCounter関数の分解能
図42

分解能の確認方法として、For~NextでQueryPerformanceCounter関数を繰り返し実行させ、且つ秒数を計算するためのQueryPerformanceCounter値をQueryPerformanceFrequency値で割り算する処理も入れています。そのため分解能は、図42の左側のように「約2.3マイクロ秒」となりました。
注目してほしいのは縦軸で、これ以外の関数のグラフでは「各秒数の度数=数百」となっていたものが、QueryPerformanceCounter関数では「各秒数の度数=1」となっています。このことから測定結果(約2.3マイクロ秒)は、「For~Next間の制御+値取得+除算計算時間」に掛かっている時間だと推定しました(この結果は私のノートPCでの結果であり、環境により異なります)。
では本当の分解能はいくつなのかを確認するため、発生した時刻の「10マイクロ秒以下の頻度」を0.1マイクロ秒単位(私のPCのQueryPerformanceFrequency値が10,000だったため)で集計したのが図42の右側です。どの秒数もほぼ均等に頻度が出ていることから「パフォーマンスカウンター値は1単位(0.1マイクロ秒ごと)」で取得可能と思われます。

9-1.受け取るデータ型について

上記では、各関数の戻り値をCurrency型で受け取りました。しかしMicrosoftのサイト等では「LARGE_INTEGER構造体」で受け取る方法を紹介しています。この構造体で受け取る方式で書いたのが図43のコードです。
  1. '========== ⇩(19) Win32APIの宣言 ============
  2. #If Win64 Then
  3.  Declare PtrSafe Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As LARGE_INTEGER) As Long
  4.  Declare PtrSafe Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As LARGE_INTEGER) As Long
  5. #Else
  6.  Declare Function QueryPerformanceFrequency Lib "kernel32" (frequency As LARGE_INTEGER) As Long
  7.  Declare Function QueryPerformanceCounter Lib "kernel32" (procTime As LARGE_INTEGER) As Long
  8. #End If
  9. '========== ⇩(20) LARGE_INTEGER構造体の宣言 ============
  10. Type LARGE_INTEGER
  11.  QuadPart As LongPtr
  12. End Type
  13. '========== ⇩(21) QueryPerformanceCounterの値 ============
  14. Sub Get_PerformanceCounter()
  15.  Dim Cnt As LARGE_INTEGER   'カウント数(起動後~)
  16.  Dim Freq As LARGE_INTEGER   '周波数(カウント数/秒)
  17.  Call QueryPerformanceFrequency(Freq)
  18.  Call QueryPerformanceCounter(Cnt)
  19.  MsgBox Cnt.QuadPart / Freq.QuadPart
  20. End Sub
図43

261~267行目のAPI関数宣言では、受け取る値のデータ型をLARGE_INTEGERとしています。そのLARGE_INTEGER型は270~272行目で宣言していますが、この中で271行目「QuadPart As LongPtr」と、LongPtr型(64ビット版はLongLong型、32ビット版はLong型)で受け取る事としてみました。
プログラムを使う全てのExcelが64ビット版ならば問題は無いのですが、32ビット版が混在している場合は問題が発生します。
64ビット版ExcelではLongLong型(64ビット)となるのですが、32ビット版ではLong型(4バイト)となり、とても「Largeとは言えないデータ型」となってしまうためです。
ちなみに図43のコードを使った場合、周波数を10,000,000だとすると、Long型では3分半で(先頭ビットに1が入るため)マイナス値となり、7分でオーバーフローしてしまう事になります。
他にも、図44のようにLowとHighの2つのLong型を合わせる方法もありますが、LowとHighの値を結合するのも大変です。
  • Type LARGE_INTEGER
  •  LowPart As Long
  •  HighPart As Long
  • End Type
図44

またDouble型も8バイトでサイズ的にはOKですが、浮動小数点のため数字が丸められる(≒四捨五入)危険性があります。
なおVariant型やString型も試しましたが、Variant型ではエラーが発生、String型ではExcelがダウンしますので使用不可です。
これらに対しCurrency型は「15桁+小数点以下4桁」の固定小数点型で、内部的には19桁の整数として管理されているようです。四捨五入の問題もありませんし、実質64ビット版のLongLong型と同じと言えます。
サイズ的にも8バイトのデータ長がありますので、「起動後2900年間」は正常に動く計算になりますし、32ビット版でも64ビット版でも同じCurrency型指定が可能(=プログラムを分ける必要が無い)ですので、今回はCurrency型を使い図40のコードとしました。
寄り道(QueryPerformanceCounterの懸念点)
その他の懸念点もいくつかあるようです。
1つ目は周波数の件で、Microsoftは「周波数は固定値」と説明していますが、CPUの負荷によりクロック周波数は変化するもののようです。WinXP以前ではその周波数をQueryPerformanceCounterのカウンター値としていた為に「カウンター数も変化」してしまう問題があったようです。しかしVista以降は一定間隔で取得できているようです。
2つ目はマルチコアの件です。最近のCPUは1つのCPUに複数のコアが入っているマルチコアが主流ですが、その内のどのコアのカウンタ値を取得するかでも多少の誤差が出るとの情報があります。しかしQueryPerformanceCounter関数の実行時間自体の方が10倍以上かかるため、あまり問題にはならない気はします。

10.TimeValue関数

Now関数の時と同様に、TimeValue関数にもVBA版とワークシート版があります。どちらも引数に時刻を文字列で指定することで、時刻を表す値を戻します。

10-1.VBAのTimeValue関数

VBAのTimeValue関数では、引数に指定できる時刻の文字列は「"0:00:00" から "23:59:59" まで」です。戻り値はDate型ですので精度は秒単位です。この戻り値を数値で表せば、時刻を表すシリアル値(1未満の小数点の値)となります。
一般的にプログラム中で停止時間を指定する場合、「Now() + TimeValue("00:00:01")」のような書き方をしますが、これは「現時刻から1秒後」を表します。
VBAのTimeValue関数で戻される値はDate型のため秒単位ですが、ミリ秒単位の値にすることも可能です。
例えば「TimeValue("00:00:01") / 10」とすれば、0.1秒のシリアル値にすることが出来ます。
またシリアル値の1は1日に相当しますので、TimeValue関数を使わずに、例えば0.1を「60秒 × 60分 × 24時間 = 86,400 」で割って「0.1 / 24 / 60 / 60」とすれば、0.1秒相当のシリアル値に出来ます。
しかし、この0.1秒の計算式を「0.1 /(24 * 60 * 60)」と、分母にまとめてしまうと「オーバーフローのエラー」となります。これは分母の丸カッコ内の「24 * 60 * 60」を計算している途中で、データ型が変わってしまうためです。
つまり、最初の掛け算である「24 * 60」の内、「24」も「60」もInteger型(~32,767)であるため、Excel側としては式の計算結果を入れる枠も自動的にInteger型で用意します。そして24 × 60 の結果の「1,440」を格納しますが、この時 1,440 はInteger型の範囲内なので問題は起こりません。
次に、その結果(Integer型の1,440 )と3番目「60(Integer型)」の掛け算を行うのですが、やはりどちらもInteger型なので、計算結果の枠もInteger型のまま 1,440 × 60 の計算をします。その計算結果は「86,400」です。
「32,767が最大のInteger型の結果枠」の場所に、Integer型の範囲を超えた値(86,400)を入れようとするので「オーバーフロー」のエラーとなるのです。
(24や60は、Byte型(0~255)と認識しても良さそうですが、マイナスの可能性があるからか整数は「32,767以下であればInteger型」と認識するようです)
このオーバーフローの対策としては「0.1 /(24& * 60 * 60)」のように、Long型を表す「&(アンパサンド)」を付けることで計算結果枠をLong型(~2,147,483,647)で確保してくれるため、エラーが回避できます。また分母を手計算で先に行っておき「0.1 / 86400」という式でもOKです。

10-2.ワークシートのTIMEVALUE関数

まずセルの書式設定では、図45のように時:分:秒と時間表示方法を指定する時に、秒(s)の後ろに「.(ピリオド)に続けて0(ゼロ)を指定」できます。このゼロの部分がミリ秒レベルを表し、ゼロを3つまで(=小数点3桁の秒)指定可能です(ゼロが3つを超えると、書式が確定できません)。
つまり「セル上では、ミリ秒の表示が可能」ですが、数式バーではミリ秒部分は表示されない(四捨五入されて秒単位で表示)ので「セル入力は可能だが、編集は不可」のようです。
セル上のミリ秒の書式設定
図45

ワークシート関数のTIMEVALUE関数はVBAのTimeValue関数と同様に、引数として基本的に「"時:分:秒"」を指定しますが、この時に「hh:mm:ss.000」のような小数点3桁のミリ秒指定が可能です。図46はミリ秒指定の様子です。
TIMEVALUE関数の表示
図46

なおTIMEVALUE関数の引数には小数点3桁目を超えても指定は可能です。しかし、計算対象の値は3桁目に四捨五入されてしまいます。ですので図47のように「23:59:59.9999」は「24:00:00.000」と四捨五入され、更に戻り値は1未満のため「00:00:00.000」と解釈され、B1セルの表示はゼロとなります。
TIMEVALUE関数の上限
図47

以上のことから、VBAのTimeValue関数はミリ秒は扱えないのに対し、「ワークシートのTIMEVALUE関数はミリ秒が可」と分かります。この機能をVBAで使用するには、ワークシートのNOW関数を使う時と同じように「Evaluateメソッド(または角カッコで囲む)」を使用します。コード例が図48です。
  1. '========== ⇩(22) TimeValueの値 ============
  2. Sub Use_TimeValue()
  3.  Dim doubleT1 As Double
  4.  Dim doubleT2 As Double
  5.  Dim doubleT3 As Double
  6.  doubleT1 = TimeValue("12:34:56") * 60 * 60 * 24
  7.  doubleT2 = Evaluate("TimeValue(""12:34:56.123"")") * 60 * 60 * 24
  8.  doubleT3 = doubleT2 + Evaluate("TimeValue(""00:00:00.001"")") * 60 * 60 * 24
  9.  MsgBox doubleT1 & vbNewLine & doubleT2 & vbNewLine & doubleT3
  10. End Sub
図48

296行目「doubleT1 = TimeValue("12:34:56") * 60 * 60 * 24」では、VBAのTimeValue関数に対して「12:34:56」を指定し、時刻を取得した後、「60 * 60 * 24」を乗算して秒単位に変換しています。
297行目「doubleT2 = Evaluate("TimeValue(""12:34:56.123"")") * 60 * 60 * 24」は、ワークシート関数のTIMEVALUE関数に対し「12:34:56.123」とミリ秒単位までの時刻を指定してシリアル値を取得しています。なお、TimeValue式全体を「"(ダブルクォーテーション)」で囲う必要があるため、時刻を囲っている「"(ダブルクォーテーション)」は二重にします。
また、Evaluateの部分は「[TimeValue("12:34:56.123")] 」と、角カッコで囲ってもOKです。
298行目「doubleT3 = doubleT2 + Evaluate("TimeValue(""00:00:00.001"")") * 60 * 60 * 24」は、267行目で得たシリアル値に対し、0.001秒を加算しています。角カッコを使うと「[TimeValue("00:00:00.001")] 」となります。
300行目「MsgBox doubleT1 & vbNewLine & doubleT2 & vbNewLine & doubleT3」では、それぞれの値を行を変えてメッセージ出力させています。図48を実行した結果が図49です。
TIMEVALUE関数の出力例
図49

1行目は「VBAのTimeValue関数」の結果です。時刻指定が秒単位ですので当然ですが、出力も秒単位となります。
2行目は「ワークシートのTIMEVALUE関数」で、ミリ秒まで指定可能で、出力もミリ秒が表示されています。
3行目は、2行目に対して0.001秒(=1ミリ秒)を加算しています。

11.時間取得関数のまとめ

以上の関数等の特徴を図50にまとめました。
関数等を使う上では「関数の実行時間」も一応考慮しておく必要があると思いますので、試して載せました。方法としてはFor~Nextで繰り返し各関数を実行させ、その実行前後の時間差から「1回当たりの実行時間」を割り出しています。またその実行時間と分解能は、比較を容易にするため「マイクロ秒(1/1,000,000 秒)」単位としています。
なお分解能や実行時間は私のノートPC(Win11 Core-i5)で測ったもので、参考値として下さい。("約"は省略しています)
取得分解能(μs)実行時間
(μs/回)
Max追加要否
秒以下
の桁数
タイマー最大値オーバー後API
宣言
ライブラリ
追加
標準
(15ms)
高精度
(1ms)
Now実日付+時刻Date01,000,0000.079999/12/31エラー
Evaluate("Now()")実日付+時刻Double215,00010,000409999年エラー
Timer実時刻Single215,0008,0000.071(24時)未満0戻り
GetLocalTime実日付+時刻SYSTEMTIME
構造体
315,0001,0001.39999年?エラー?
SWbemLocator実日付+時刻String6(実3)15,00013,00012,0009999年?エラー?
timeGetTime起動後ミリ秒整数値315,0001,0001.349.7日0戻り
timeGetSystemTime起動後ミリ秒MMTIME
構造体
315,0001,7001.349.7日0戻り?
GetTickCount起動後ミリ秒整数値315,0001.349.7日0戻り?
QueryPerformanceCounter起動後カウント数整数値(7)(0.1)1.3型による?
図50

分解能と実行時間を比較すると分かるように、(SWbemLocatorを除き)実行時間は分解能に影響を与える程は大きくないことが分かります。ですので、必要な分解能から時間取得をする関数を選べば良いことになります。
他にも、コマンドプロンプトから「Echo %time%」を実行することでもミリ秒単位の時刻が取得できますし、またPowerShellを使う方法もありそうです。しかしこれらの方法をExcel VBAから実行する場合、明らかに時間が多く掛かっており、ミリ秒どころか秒単位も怪しい感じなので今回は割愛しました。

アプリ実例・関連する項目

再計算されたか否かのチェックをイベントで取得
ワークシート上で永久カレンダーを作る
セルへの日付入力をカレンダー日付クリックで選定する
図形も貼り付けられるスケジュール帳
セル色変更の情報をイベント風に受け取る
西暦・和暦対照表
図形で作るアナログ時計
年月をスクロールバーで選択する予定表ひな型
CSVファイルでデータを読み書きする月間予定表
流れる文字列
ExcelシートDBとSQLを使った倉庫管理システム
セル変更履歴をコメントに残す
初回入力日時を保存できるワークシート関数
DVD等の内容・保管場所等管理システム
ラベルカレンダーをクリックし日付入力
祝日を自動反映するカレンダー
OLEObjectのラベルカレンダー(同一ブック内)
OnTimeメソッドの第一・第二パラメータ設定方法
自動的に閉じるメッセージ

サンプルファイル

日時や時間の取得(its-045.xlsm)
セキュリティ向上を目的として「インターネット経由でダウンロードしたOfficeファイル(Excel等)のマクロは、既定でブロック」されるようにOfficeアプリケーションの既定動作が変更になりました。(2022年4月より切替開始)
解除の方法については「ダウンロードファイルのブロック解除方法」を参照下さい。