Motivation
I am interested in optimising the execution time of code with short (fast) execution times on the order of microseconds (µs), but that will be used with high frequency (e.g. like min(array)
and max(array)
or arrays accessed through a class interface). The code execution time can then be shorter than the resolution of the timers used to time it... A similar concern was noted in A rather accurate VBA stopwatch.
I have written some code to test the timers in VBA, and provided below an analysis of what the code measures to help understand timing code-execution in VBA excel. There are a number of timers available (see references to other reviews and questions/weblinks) and I coded these up to test their resolution and timer_call
execution time, on a Mac machine with Office 16.
I would appreciate reviews of the timer analysis & code below, as well as suggestions to generate a better understanding of how profiling works, what is actually measured and how accurate are the execution times.
Also interested to see if there are other timer methods (on Mac and WinOS), and whether the code is compatible with Windows Excel. Finally, as far as I can see, the impact of CPU load on the execution times measured generates a rather high variability (~5%). Advice on how to minimise that is very welcome (on both Mac and Windows).
TLDR...CONCLUSION
4 different timers were tested: gettickcount
, vba.Timer
,
evaluate("now()")
and QueryPerformanceTimers
(QPC). The last of these was tested directly
as well as encapsulated in a profiler helper function and class.
The timers tested all have different resolutions and timer execution times,
with the vba.timer
having a variable resolution depending on the time of the day
The QPC method provides the accuracy required measure execution time directly
even for code that executes within 1 s. Encapsulating the QPC function in a class or function
reduces the resolution and increases the timer execution time to 1 to 2 µs (from 0.1 µs)
Using QPC a constant execution time function was created for the timer tests. Due to CPU use by other processes, the reproducibility of this function execution times is ~5%, but this may differ on different machines, as well as different nature of the operations executed.
Two criteria were developed to check appropriate use of timers:
t_codecall> 0.05 * t_timercall
To measure the code execution time accurately, it must be larger than the error in the timer execution time.n >1/0.05 * tres/max(t_timercall, t_codecall)
The noise due to the resolution istres/n
, and must be smaller than the reproducibility in the largest of the code or timer execution time.
Provided both criteria have been met, the code execution time may be approximated as
t_codecall = sum{(t_codecall+t_timercall)_i | i=1...n}/n-t_timercall
error: +/-0.05*(t_codecall+t_timercall)+tres/n
This means that when t_codecall ~ t_timercall
the profiling system should calibrate the timer execution time as any function UDF call is at least 1 µs; direct QPC can always be used, but embedding QPC in a function requires at least ~n=20
.
Using the code on different machines show some quite large differences depending on whether you run Excel 32, 64, on windows64 or Excel 64 on Mac64. In particular, gettickcounts resolution time goes up to 16 milliseconds on a wind0ws machine from 1 on the Mac, and dll calls in 64 excel seem to take much longer then on Excel 32.
DETAIL
Basic principle of code timing implemented
Below I set out my analysis of timers as well as the code used (works in both Mac and Windows 64-bit systems excel16).
In its crudest form, the time code runs is measured using a timer function that returns a value that increases linearly with time. This is then implemented by calling the timer before, and after the code to be timed:
t0 = Timer
run code
t1 = Timer
dt1 = t1 - t0
We can depict this as
fast sampling: .....vo^cccccvo^.....
slow sampling: ..v..o.^cccccv..o.^..
Here the code goes
- to the timer-sub at 'v',
- takes the timestamp at 'o' (at some point inside the timer sub) and
- returns from the timer sub at '^'
So the time between the two time stamps, 'o.^cccccv..o' is the result of
- returning from timer after the time stamp is taken ('o.^')
- the time for sample code (
ccccc
) - the time going into the timer sub until the time stamp is taken ('v..o')
The elapsed time measured is the sum of the time it take to run the timer, and the code once:
dt1 = dt_timercall + dt_code
If we run the code sample n times the total time passed is:
dtn = n * dt1
Timer resolution and call length
If the resolution of the timer is the smallest time step the timer will take
the CounterCalibration
routine measures the resolution for the following timers:
gettickcount
increases by 1 every millisecond.
Thedt_timercall
is 0.1 microsecond (µs).vba.Timer increases by ~1 (at 1 am) to ~8 millisecond (just before midnight) note the changing resolution is due to the lowest resolution of a single which increases as the timer value increases towards midnight.
Itsdt_timercall
is ~3 microseconds.evaluate("now()") increases by 10 milliseconds.
Itsdt_timercall
is 300 microseconds (the slowest by far).QPC timers (
QueryPerformanceCounter
) access the crystal pulse counter directly, and increase by a minimum of ~0.1 microsecond (µs) determined by the time the call to QPC takes.
Resolution anddt_timercall
of a QPC call are thus equal at ~0.1 µs.cprofile
implements QPC but embedded in a helper class that on termination calls QPC to work out how much time has passed since initialisation this is used to profile functions with a single line of code (as garbage collection will ensure the terminate method is called).
Resolution anddt_timercall
are equal at ~1 µs.fprofile implements QPC in a function that returns the time passed between two successive calls.
Resolution anddt_timercall
are equal at ~1 µs.
The impact of resolution and timer call on timing code
The routine CouterTests(n)
uses the different timers to measure the time it takes to run the Fixedtime
n times. Note this function always exits after a fixed amount of time (as measured with QPC). The CouterTests(n)
is called 5 times with increasing values of n from the master routine EvaluateCounters
that also handles the output:
- results for MAC machine
Mac64
fixed time target: 20
Repeats: average 20.0 stdev 1.36 min: 19.6 max: 57.9
Comparing timing functions <-- dt_timercall + dt_code in mus -->
count : tRes tTmrCll 1 10 100 1000 5000
Timer : 7813 2.47 0 0 78.1 23.4 21.9
eval(now) : 10000 234 0 1000 200 240 262
gettickcount: 1000 0.0914 0 0 20 21 20
QPC : 0.0884 0.0884 21.1 20.0 19.6 19.9 19.6
- cprofile : 1.72 1.72 26.0 23.1 21.3 21.5 21.5
- fprofile : 1.31 1.31 22.4 22.2 21.0 21.1 21.2
NOTE 1: CounterCalibrate
finds the resolution (tRes
) and dt_timercall
(tTmrCll
);
results in column 1 and 2.
NOTE 2: CounterTest
estimates the fixedtime function using n (given in top row);
results in last 5 cols.
NOTE 3: all times are for a MacBook Pro
2.7 GHz Dual-Core Intel Core i5; Retina, 13-inch, Early 2015.
- results for Windows machines
I now also ran the code on a windows machine for comparison. The code works fine, but the performace is weaker to say the least....
- Windows 64 Excel 32
Win32
fixed time target: 20
Repeats: average 18.3 stdev 0.365 min: 18.2 max: 32.2
Comparing timing functions <-- dt_timercall + dt_code in mus -->
count : tRes tTmrCll 1 10 100 1000 5000
Timer : 15625 0.116 0 0 0 15.6 18.8
eval(now) : 15400 46.8 0 1000 200 30.0 48.0
gettickcount: 15620 0.00941 0 0 0 16 18.8
QPC : 0.100 0.0510 18.5 18.3 18.3 18.3 18.3
- cprofile : 2.18 2.18 21.5 19.3 19.3 19.1 19.1
- fprofile : 0.180 0.180 18.6 18.5 18.4 18.4 18.4
Note: on a Intel(R) Xeon(R) CPU E5-2687W v3 @ 3.10GHz (2 processors), 64 GB RAM, 64-bit operating system, x64-based processor running Microsoft® Excel® 2016 (16.0.266.1000) MSO 32 bit (dont ask why...)
Note 2: no idea why the class implementation (cprofile) takes so much longer then encapsulating QPC in a function (fprofile)
Note 3: the QPC timer now occasionally returns the same count twice in a row, and the resolution is no longer equal to the timer call!
- Windows 64 Excel 64
Win64
fixed time target: 20
Repeats: average 26.0 stdev 2.67 min: 23.2 max: 70.5
Comparing timing functions <-- dt_timercall + dt_code in mus -->
count : tRes tTmrCll 1 10 100 1000 5000
Timer : 3906 0.0789 0 391 0 23.4 23.4
eval(now) : 10000 35.5 0 0 200 70.0 62.0
gettickcount: 15640 2.50 0 0 0 0 19
QPC : 2.51 2.51 25.5 25.4 25.9 25.9 25.8
- cprofile : 3.00 3.00 28.5 28.5 26.5 26.3 26.2
- fprofile : 2.64 2.64 25.6 25.5 26.4 25.9 25.9
Note 1: Surface Pro 16GB, Intel(R) Core(TM) i7-7660U CPU @ 2.50GHz 2.50 GHz; Windows 10 Pro, 21H2, OS.buidt 19044.1586 running Microsoft® Excel® for Microsoft 365 MSO (Version 2202 Build 16.0.14931.20128) 64-bit
Note 2: no idea why the dll calls in 64 take so long... but in 2002 it was 20ms (https://jeffpar.github.io/kbarchive/kb/172/Q172338/)
Note 3: QPC respond with a new count every call; e.g. similar to the behaviour on the Mac, but the calls take ~20 longer then on the Mac...
Reproducibility of execution time
The average and variance of the execution time of the FixedTime
function are measured by running the function 1000 times. The result, given in the first lines of the output, is still quite variable. At 20 ±1 µs with outliers well above this I believe this to be because Excel is not the only activity, and other processes compete for the CPU. So its reasonable to assume that execution time in general has a variability of ~5%; Note, this variability may be vary strongly depending on the nature of operations (e.g. disk access).
So when measuring code execution time, however accurate, the CPU variability causes a variance in execution times that causes an error in both t_timercall
and t_codecall
of about ~5% (on this Mac).
Understanding the timer results as function of n
We can compare the resolution, and the total lapsed time:
(nSteps+1) +/- 1 = dtn / tres
We add +1 as we are unlikely to exactly fit nsteps
in dtn
, and the error is ±1 step. if nSteps < n
, then frequently after the Fixedtime
function is executed the 2nd timer call see that counter is not increased and the lapsed time dt1
is thus zero. Occasionally the code execution interval may cross the time where the timer increments by tres
, and a non-zero lapsed time results, i.e.dt1=tres
If nSeen
is the number of times the timer returns a non-zero value for dt1, then for the n executions of FixedTime
, we perceive the lapsed time to be:
dtn~(nSeen+1)*tres (for nSeen<<n)
and even though the function is executed in less time than resolution of the timer we can still work out an estimate of Fixedtime
's execution time:
dt1 = (nSeen + 1 +/-1) * tres / n
Error estimate for code time measurement
The error of the determination of tp1
is associated with the resolution the value of n*dt1
is estimated with (nSeen+1 +/- 1)*tres
.
The error in n*dt1 is +/- tres
.
Hence the measurement error in dt1 is +/- tres/n
.
For the various timers with n=1000:
o QPC timer : time error = 0.1 / 1000 = 0.0001 µs
- in class or func: time error = 1 / 1000 = 0.001 µs
o gettickcount timer: time error = 1000 / 1000 = 1 µs
o vba.Timer timer : time error = 4000 / 1000 = 4 µs
o eval(now) timer : time error = 10000 / 1000 > 10 µs
To work out the code execution time we include all the errors:
t_codecall=dt1-t_timercall +/- 0.05*t_codecall +/-0.05 t_timercall +/- tres/n
estimating the error for the various timers at n=1000 and tcodecall=20 µs:
o QPC timer : time error = 20*0.05 + 0.1 / 1000 +0.1*0.05 = 1.005 µs
- in class or func: time error = 20*0.05 + 1 / 1000 + 1*0.05 = 1.05 µs
o gettickcount timer: time error = 20*0.05 + 1000 / 1000 +0.1*0.05 = 2.005 µs
o vba.Timer timer : time error = 20*0.05 + 4000 / 1000 +2.5*0.05 = 5.6 µs
o eval(now) timer : time error = 20*0.05 +10000 / 1000 +240*0.05 = 23 µs
Alternative approach, measure n executions between two timer calls
If we change our timing method to:
t1 = Timer
For i = 1 To n
fixedtime
Next
t2 = Timer
We change the lapsed time between the two timer calls:
dtn = dt_timercall + n * dt_codecall
dt1 = dtn / n = dt_timercall / n + dt_codecall
Note that this alternative would give the following errors:
dtn=nSeen*tres
dt1=(nSeen+1)*tRes/n +/-tRes/n +/-0.05(dt_timercall/n + dt_codecall)
The accuracy of the result improves, as with increasing n, dt1
would be more and more representative of dt_codecall
with the impact of the error in t_timercall
reducing to 0 for n→∞
For Eval(now)
, dt_timercall = ~250 µs, and with n=1000
dt1=0.25+dt_codecall+ 0.05(0.25+20)+tres/n
which would be a lot better than the hundreds of µs measured by CounterTest
. Note this way of testing is okay for single functions, but it is not suitable to profile multiple functions in a complex piece of software.
Criteria for n or t_codecall
to ensure proper timing of code
Based on the above there are two criteria that may help assess appropriate ways to use a timer assuming the execution reproducibility is 5%:
a) t_codecall>> 0.05 * t_timercall
otherwise there is the code execution time is lost in the noise of the timer call execution time .
b) 0.05*max(t_timercall, t_codecall) >> tres/n
or so that noise due to the resolution is negligible. Rearranging:
n>20*tres/max(t_timercall, t_codecall)
For the different timers at n=1000 and t_codecall=20 µs:
o eval(now) a) t_codecall>12.5 b) 20*10000/250: n > 800
o gettickcount a) t_codecall>0.005 b) 20*1000/20 : n > 1000
o QPC a) t_codecall>0.005 b) 20*0.1/20 : n > 0.1
- in cls or fun a) t_codecall>0.05 b) 20*1/20 : n > 1
Calculation of code execution time
Provided both criteria have been met, the code execution time may be approximated as
t_codecall = sum{(t_codecall+t_timercall)_i | i=1...n}/n-t_timercall
error: +/-0.05*(t_codecall+t_timercall)+tres/n
this means that when t_codecall ~ t_timercall
the profiling system should calibrate the timer execution time as any function UDF call is at least 1 mus, direct QPC can always be used, but embedding QPC in a function requires at least ~n=20
to measure a 1 µs function.
Notes and references
gettickcount
on the Mac used to be inmicrosoftoffice.framework
, but this disappeared. Checking theexcel.app
package on the Mac showed several newmsoxx.framework
; these can be opened by text edit and mso20 seemed to holdgettickcount
...- conditional block:
https://sysmod.wordpress.com/2016/09/03/conditional-compilation-vba-excel-macwin3264/ - see also https://stackoverflow.com/questions/198409/how-do-you-test-running-time-of-vba-code
- for QualityPerformanceCounters
- these were also found in
mso20
.... - https://docs.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps#low-level-hardware-clock-characteristics
- win machines: https://github.com/jonadv/VBA-Benchmark
- these were also found in
- there is also a Stack Exchange review
VBA Code Profiling
'start of Code: add to a module
'MODULE NAME: TimeCounters
'by vbAdder April 2022
Option Explicit
Option Private Module
Option Compare Text
'notes:
' o gettickcount on the mac used to be in microsoftoffice.framework, but this disappeared in office 2016
' Checking the excel.app package on the mac showed several new msoxx.framework,
' these can be opened by text edit and mso20 seemed to hold gettickcount...
' o conditional block:
' https://sysmod.wordpress.com/2016/09/03/conditional-compilation-vba-excel-macwin3264/
' o see also https://stackoverflow.com/questions/198409/how-do-you-test-running-time-of-vba-code
' o for QualityPerformanceCounters
' - these were also found in mso20....
' - https://docs.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps#low-level-hardware-clock-characteristics
' - win machines: https://github.com/jonadv/VBA-Benchmark
' o there is also a stack exchange review
' https://codereview.stackexchange.com/questions/70247/vba-code-profiling/275445#275445
Private Freq As Currency 'QPC system counter crystal frequency
Public SSS As Currency
#If Mac Then
#If MAC_OFFICE_VERSION >= 15 Then
#If VBA7 Then ' 64-bit Excel 2016 for Mac, note the new location of the microsoftoffice framwwork (mso20)
Private Declare PtrSafe Function gettickcount Lib "/Applications/Microsoft Excel.app/Contents/Frameworks/mso20.framework/mso20" Alias "GetTickCount" () As LongPtr
Private Declare PtrSafe Function QueryPerformanceCounter Lib "/Applications/Microsoft Excel.app/Contents/Frameworks/mso20.framework/mso20" (stamp As Currency) As Byte
Private Declare PtrSafe Function QueryPerformanceFrequency Lib "/Applications/Microsoft Excel.app/Contents/Frameworks/mso20.framework/mso20" (Freq As Currency) As Byte
Function isSystem(): isSystem = "Mac64": End Function
#Else ' 32-bit Excel 2016 for Mac, not sure here, you might try mso20,
Private Function QueryPerformanceCounter(): Err.Raise 1111, "profiler", "Code not compatible with bit32": End Function
Private Function gettickcount(): Err.Raise 1111, "profiler", "Code not compatible with bit32": End Function
Private Function QueryPerformanceFrequency(): Err.Raise 1111, "profiler", "Code not compatible with bit32": End Function
Function isSystem(): isSystem = "Mac32": End Function
#End If
#Else
Private Function QueryPerformanceCounter(): Err.Raise 1111, "profiler", "Code not compatible with bit32": End Function
Private Function gettickcount(): Err.Raise 1111, "profiler", "Code not compatible with bit32": End Function
Private Function QueryPerformanceFrequency(): Err.Raise 1111, "profiler", "Code not compatible with bit32": End Function
Function isSystem(): isSystem = "Mac32 <v15": End Function
#End If
#Else
#If Win64 Then ' Excel 2010 or later for Windows...NOT TESTED!!
Private Declare PtrSafe Function gettickcount Lib "kernel32" Alias "GetTickCount" () As LongPtr
Private Declare PtrSafe Function QueryPerformanceCounter Lib "kernel32" (stamp As Currency) As Byte
Private Declare PtrSafe Function QueryPerformanceFrequency Lib "kernel32" (Freq As Currency) As Byte
Function isSystem(): isSystem = "Win64": End Function
#Else ' pre Excel 2010 for Windows
Private Declare PtrSafe Function gettickcount Lib "kernel32" Alias "GetTickCount" () As LongPtr
Private Declare PtrSafe Function QueryPerformanceCounter Lib "kernel32" (stamp As Currency) As Byte
Private Declare PtrSafe Function QueryPerformanceFrequency Lib "kernel32" (Freq As Currency) As Byte
Function isSystem(): isSystem = "Win32": End Function
#End If
#End If
Sub EvaluateCounters()
application.EnableEvents = False
application.ScreenUpdating = False
application.Calculation = xlCalculationManual
'initialise QPC
QueryPerformanceFrequency Freq
Dim dt: dt = 20: fixedtime dt 'set fixed time of function to 20 mus
Debug.Print
Debug.Print isSystem
Debug.Print "fixed time target:" + num2str(dt)
machineerror 1000
'run tests
Const nRows = 7
Const Nt = 5
Dim j, i, dta(0 To nRows, -2 To Nt)
dta(0, -1) = "tRes": dta(0, 0) = "tTmrCll": dta(0, 1) = 1: dta(0, 2) = 10: dta(0, 3) = 100: dta(0, 4) = 1000: dta(0, 5) = 5000
CounterCalibration dta, 50
For j = 0 To nRows: dta(j, -2) = dta(j, -2) + num2str(dta(j, -1)): Next
For j = 0 To nRows: dta(j, -2) = dta(j, -2) + num2str(dta(j, 0)): Next
For i = 1 To Nt
CounterTests dta, i
For j = 0 To nRows: dta(j, -2) = dta(j, -2) + num2str(dta(j, i)): Next
Next
'report
Debug.Print
Debug.Print "Comparing timing functions <-- dt_timercall + dt_code in mus --> "
i = 0:
Debug.Print "count :"; dta(i, -2): i = i + 1
Debug.Print "Timer :"; dta(i, -2): i = i + 1
Debug.Print "eval(now) :"; dta(i, -2): i = i + 1
Debug.Print "gettickcount:"; dta(i, -2): i = i + 1
Debug.Print "QPC :"; dta(i, -2): i = i + 1
Debug.Print " - cprofile :"; dta(i, -2): i = i + 1
Debug.Print " - fprofile :"; dta(i, -2): i = i + 1
application.EnableEvents = Not False
application.ScreenUpdating = Not False
application.Calculation = xlCalculationAutomatic
End Sub
Private Sub CounterCalibration(dta, Ntot) 'calculate resolution times
Dim i
Dim id: id = 0
'clear all events
For i = 1 To 23
DoEvents
Next
Dim Nok, s As Double, z As Single, d As Double
'"for each timer" in mus
Nok = 0: s = 0: i = 0
Dim x As Single, x2 As Single
While Nok < Ntot
i = i + 1: x = Timer: x2 = Timer
z = (x2 - x)
If z > 0 Then
s = s + z: Nok = Nok + 1
End If
Wend
dta(1, -1) = s * 1000000 / Nok
dta(1, 0) = s * 1000000 / i
'"for each eval(now)" in dt format
Nok = 0: s = 0: i = 0
Dim d1 As Double, d2 As Double
While Nok < Ntot
i = i + 1: d1 = Evaluate("now()"): d2 = Evaluate("now()")
d = d2 - d1
If d > 0 Then s = s + d: Nok = Nok + 1
Wend
dta(2, -1) = s * 24 * 3600000000# / Nok
dta(2, 0) = s * 24 * 3600000000# / i
'"for each gettickcount" if milli s ticks
Nok = 0: s = 0: i = 0
Dim L1 As LongPtr, L2 As LongPtr
While Nok < Ntot
i = i + 1: L1 = gettickcount: L2 = gettickcount
d = L2 - L1
If d > 0 Then
s = s + d: Nok = Nok + 1
End If
Wend
dta(3, -1) = s * 1000 / Nok
dta(3, 0) = s * 1000 / i
' "for each QPC" in counts of periods
Dim s1 As Currency, s2 As Currency
Nok = 0: s = 0: i = 0
While Nok < Ntot
i = i + 1: QueryPerformanceCounter s1: QueryPerformanceCounter s2
d = s2 - s1
If d > 0 Then s = s + d: Nok = Nok + 1
Wend
dta(4, -1) = s * 1000000 / Freq / Nok
dta(4, 0) = s * 1000000 / Freq / i
'"for each timer" in mus
Nok = 0: s = 0: i = 0
Dim p As cProfile
While Nok < Ntot
i = i + 1: Set p = New cProfile: Set p = Nothing
d = SSS
If d > 0 Then
s = s + d: Nok = Nok + 1
End If
Wend
dta(5, -1) = s * 1000000 / Freq / Nok
dta(5, 0) = s * 1000000 / Freq / i
'"for each timer" in mus
Nok = 0: s = 0: i = 0
While Nok < Ntot
i = i + 1: fprofile: d = fprofile
If d > 0 Then
s = s + d: Nok = Nok + 1
End If
Wend
dta(6, -1) = s / Nok
dta(6, 0) = s / i
End Sub
Private Sub CounterTests(dta, id) 'calculate doevents inmus
'clear all events
Dim i, N: N = dta(0, id)
For i = 1 To N
DoEvents
Next
Dim x1 As Single, x2 As Single, d As Double
'"for each timer" in s
For i = 1 To N
x1 = Timer
fixedtime
x2 = Timer
x1 = (x2 - x1)
dta(1, id) = dta(1, id) + x1
Next
dta(1, id) = dta(1, id) * 1000000 / N
'"for each eval(now)" in dt format
Dim d1 As Double, d2 As Double
For i = 1 To N
d1 = Evaluate("now()")
fixedtime
d2 = Evaluate("now()")
d = d2 - d1
dta(2, id) = dta(2, id) + d
Next
dta(2, id) = dta(2, id) * 24 * 3600000000# / N
'"for each gettickcount" if milli s ticks
Dim L1 As LongPtr, L2 As LongPtr
For i = 1 To N
L1 = gettickcount
fixedtime
L2 = gettickcount
d = L2 - L1
dta(3, id) = dta(3, id) + d
Next
dta(3, id) = dta(3, id) * 1000 / N
'"for each QPC" if milli s ticks
Dim s1 As Currency, s2 As Currency
For i = 1 To N
QueryPerformanceCounter s1
fixedtime
QueryPerformanceCounter s2
d = s2 - s1
dta(4, id) = dta(4, id) + d
Next
dta(4, id) = dta(4, id) / Freq * 1000000 / N
' "for each cprofile" in counts of periods
Dim p As cProfile
For i = 1 To N
Set p = New cProfile
fixedtime
Set p = Nothing
d = SSS
dta(5, id) = dta(5, id) + d
Next
dta(5, id) = dta(5, id) / Freq * 1000000 / N
' "for each fprofile" in counts of periods
For i = 1 To N
fprofile
fixedtime
d = fprofile
dta(6, id) = dta(6, id) + d
Next
dta(6, id) = dta(6, id) / N
End Sub
Private Sub machineerror(N)
' "for each QPC" in counts of periods
Dim s1 As Currency, s2 As Currency
Dim s As Double, ss As Double, dta, smin, smax
ReDim dta(1 To N)
Dim i, j, d As Double
s = 0: ss = 0: smax = 0: smin = 1E+99
For i = 1 To N
For j = 1 To 6
QueryPerformanceCounter s1
fixedtime
QueryPerformanceCounter s2
d = s2 - s1
d = d * 1000000# / Freq
smin = IIf(d < smin, d, smin)
smax = IIf(d > smax, d, smax)
s = s + d
ss = ss + (d) ^ 2
dta(i) = dta(i) + num2str(d)
Next
'Debug.Print dta(i)
Next
Debug.Print "Repeats: average"; num2str(s / N / 6); "stdev"; num2str((ss / N / 6 - (s / N / 6) ^ 2) ^ 0.5) + _
"min:" + num2str(smin) + "max:" + num2str(smax)
End Sub
Private Function fixedtime(Optional dt)
' E(dt)=30 = 28+2 mus for function call
Static ds: If ds = 0 Then ds = (20 - 2) / 1000000# * Freq:
If Not IsMissing(dt) Then ds = (dt - 2) / 1000000# * Freq: Exit Function
Dim s1 As Currency: QueryPerformanceCounter s1
Dim s2 As Currency: s2 = s1
While s2 - s1 < ds
QueryPerformanceCounter s2
Wend
End Function
Private Function fprofile()
Static s As Currency
If s = 0 Then QueryPerformanceCounter s: Exit Function
Dim s2 As Currency: QueryPerformanceCounter s2
Dim d As Double: d = s2 - s
fprofile = d / Freq * 1000000
s = 0
End Function
The QPC encapsulated in a minimal class
' CLASSNAME cProfile
'
' Basic class to encapsulate a QPC timer
' o timer is initialised at the end of the initialize method
' o the timer ends as soon as the terminate method is called
'
'
Option Explicit
Option Compare Text
#If Mac Then
#If MAC_OFFICE_VERSION >= 15 Then
#If VBA7 Then ' 64-bit Excel 2016 for Mac, note the new location of the microsoftoffice framwwork (mso20)
Private Declare PtrSafe Function QueryPerformanceCounter Lib "/Applications/Microsoft Excel.app/Contents/Frameworks/mso20.framework/mso20" (stamp As Currency) As Byte
#Else ' 32-bit Excel 2016 for Mac, not sure here, you might try mso20,
Private Function QueryPerformanceCounter(): Err.Raise 1111, "profiler", "Code not compatible with bit32": End Function
#End If
#Else
Private Function QueryPerformanceCounter(): Err.Raise 1111, "profiler", "Code not compatible with bit32": End Function
#End If
#Else
#If Win64 Then ' Excel 2010 or later for Windows...NOT TESTED!!
Private Declare PtrSafe Function QueryPerformanceCounter Lib "kernel32" (stamp As Currency) As Byte
#Else ' pre Excel 2010 for Windows or 32 bit installs on 64 bit systems
Private Declare PtrSafe Function QueryPerformanceCounter Lib "kernel32" (stamp As Currency) As Byte
#End If
#End If
Private s1 As Currency
Private Sub Class_Initialize()
QueryPerformanceCounter s1
End Sub
Private Sub Class_Terminate()
QueryPerformanceCounter TimeCounters.SSS
TimeCounters.SSS = TimeCounters.SSS - s1
End Sub
Code for converting numbers to strings
'sigd is the number of significant digits
'padding is the number of char in which the number will be centered (e.g. ' 9,32 '). default is sigd+6
Function num2str(itm, Optional sigd As Long = 3, Optional padding As Integer = -1)
On Error GoTo 1
Dim rtn: If IsEmpty(itm) Then rtn = ".": GoTo padit
'check and convert values
Dim v: v = CDbl(itm)
Dim av: av = Abs(v)
sigd = IIf(1 > CLng(sigd), 1, CLng(sigd))
'work out the order of magn, and the least sign value of interest
Dim nint: nint = Int(Log(av) / Log(10)) + 1
Dim nfrac: nfrac = sigd - nint 'n zeroes after . ;
'when nfrac<0 all sigd are above .
Dim UseExp As Boolean:
UseExp = nint > sigd + 4 '12345xxx wider the '1.sigdE+1'
UseExp = UseExp Or nfrac + 2 > sigd + 4 ' 0,0000xxx wider, then 1.sigdE+1
Dim UseInt As Boolean 'all digits above . are displayed,
'e.g 1234567.77 should be 1234568, not 1235000
UseInt = (Int(v) = v) Or nint >= sigd
UseInt = UseInt And Not UseExp
Dim fmtstrfrac
Select Case True
Case UseExp '0.sigdE+0
rtn = Format(Round(v / (10) ^ (nint), sigd) * (10) ^ (nint), _
String(1, "0") + "." + String(sigd - 1, "0") + "E+0")
Case UseInt
rtn = Format(v, "0")
Case nfrac > 0 '00.00sigd
rtn = Format(Round(v / (10) ^ (nint), sigd) * (10) ^ (nint), _
String(Max(1, nint), "0") + "." + String(nfrac, "0"))
Case Else
rtn = CStr(v)
End Select
'padd
padit:
If padding = -1 Then padding = sigd + 6
If padding > 2 Then
Dim i: i = v < 0 'make sure -0.2 is aligned w 0.2
Dim nL, Nr
nL = Max(0, padding - Len(rtn))
Nr = nL \ 2: nL = nL - Nr
If v < 0 Then rtn = Space(Nr) + rtn + Space(nL) _
Else rtn = Space(nL) + rtn + Space(Nr)
End If
num2str = rtn
If False Then
1 rtn = CStr(itm)
GoTo padit
End If
End Function