-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathCodeTimers.cs
369 lines (338 loc) · 15 KB
/
CodeTimers.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
/* Copyright (c) Microsoft Corporation. All rights reserved. */
/* AUTHOR: Vance Morrison Date : 10/20/2007 */
using System;
using System.Collections.Generic;
using System.Diagnostics;
/* files are best browsed in 'outline form'. You outline code with Ctrl-M Ctrl-O. */
namespace PerformanceMeasurement
{
/// <summary>
/// Stats represents a list of samples (floating point values) This class can calculate the standard
/// statistics on this list (Mean, Median, StandardDeviation ...)
/// </summary>
public class Stats : IEnumerable<float>
{
public Stats() { data = new List<float>(); }
public void Add(float dataItem) { statsComputed = false; data.Add(dataItem); }
public void RemoveRange(int index, int count)
{
data.RemoveRange(index, count);
statsComputed = false;
}
internal void Adjust(float delta)
{
statsComputed = false;
for (int i = 0; i < data.Count; i++)
data[i] += delta;
}
public int Count { get { return data.Count; } }
public float this[int idx] { get { return data[idx]; } }
public IEnumerator<float> GetEnumerator() { return data.GetEnumerator(); }
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return data.GetEnumerator(); }
public float Minimum { get { if (!statsComputed) ComputeStats(); return minimum; } }
public float Maximum { get { if (!statsComputed) ComputeStats(); return maximum; } }
public float Median { get { if (!statsComputed) ComputeStats(); return median; } }
public float Mean { get { if (!statsComputed) ComputeStats(); return mean; } }
public float StandardDeviation { get { if (!statsComputed) ComputeStats(); return standardDeviation; } }
public override string ToString()
{
if (!statsComputed)
ComputeStats();
return "mean=" + mean.ToString("f3") + " median=" + median.ToString("f3") +
" min=" + minimum.ToString("f3") + " max=" + maximum.ToString("f3") +
" sdtdev=" + standardDeviation.ToString("f3") + " samples=" + Count.ToString();
}
#region privates
public void ComputeStats()
{
minimum = float.MaxValue;
maximum = float.MinValue;
median = 0.0F;
mean = 0.0F;
standardDeviation = 0.0F;
double total = 0;
foreach (float dataPoint in this)
{
if (dataPoint < minimum)
minimum = dataPoint;
if (dataPoint > maximum)
maximum = dataPoint;
total += dataPoint;
}
if (Count > 0)
{
data.Sort();
if (Count % 2 == 1)
median = this[Count / 2];
else
median = (this[(Count / 2) - 1] + this[Count / 2]) / 2;
mean = (float)(total / Count);
double squares = 0.0;
foreach (float dataPoint in this)
{
double diffFromMean = dataPoint - mean;
squares += diffFromMean * diffFromMean;
}
standardDeviation = (float)Math.Sqrt(squares / Count);
}
statsComputed = true;
}
List<float> data;
float minimum;
float maximum;
float median;
float mean;
float standardDeviation;
bool statsComputed;
#endregion
};
/// <summary>
/// The CodeTimer class only times one invocation of the code. Often, you want to collect many samples so
/// that you can determine how noisy the resulting data is. This is what MultiSampleCodeTimer does.
/// </summary>
public class MultiSampleCodeTimer
{
public MultiSampleCodeTimer() : this(1) { }
public MultiSampleCodeTimer(int sampleCount) : this(sampleCount, 1) { }
public MultiSampleCodeTimer(int sampleCount, int iterationCount)
{
SampleCount = sampleCount;
timer = new CodeTimer(iterationCount);
timer.Prime = false; // We will do the priming (or not).
Prime = true;
}
public MultiSampleCodeTimer(MultiSampleCodeTimer template)
: this(template.SampleCount, template.IterationCount)
{
OnMeasure = template.OnMeasure;
}
/// <summary>
/// If true (the default), the benchmark is run once before the actual measurement to
/// insure that any 'first time' initialization is complete.
/// </summary>
public bool Prime;
/// <summary>
/// The number of times the benchmark is run in a loop for a single measument.
/// </summary>
public int IterationCount { get { return timer.IterationCount; } set { timer.IterationCount = value; } }
/// <summary>
/// The number of measurments to make for a single benchmark.
/// </summary>
public int SampleCount;
/// <summary>
/// The smallest time (in microseconds) that can be resolved by the timer).
/// </summary>
public static float ResolutionUsec { get { return 1000000.0F / Stopwatch.Frequency; } }
public delegate void MeasureCallback(string name, int iterationCount, float scale, Stats sample);
/// <summary>
/// OnMeasure is signaled every time a Measure() is called.
/// </summary>
public event MeasureCallback OnMeasure;
public Stats Measure(string name, Action action)
{
return Measure(name, 1, action, null);
}
/// <summary>
/// The main measurment routine. Calling this will cause code:OnMeasure event to be
/// raised.
/// </summary>
/// <param name="name">name of the benchmark</param>
/// <param name="scale">The number of times the benchmark is cloned in 'action' (typically 1)</param>
/// <param name="action">The actual code to measure.</param>
/// <returns>A Stats object representing the measurements (in usec)</returns>
public Stats Measure(string name, float scale, Action action)
{
return Measure(name, scale, action, null);
}
/// <summary>
/// The main measurment routine. Calling this will cause code:OnMeasure event to be
/// raised.
/// </summary>
/// <param name="name">name of the benchmark</param>
/// <param name="scale">The number of times the benchmark is cloned in 'action' (typically 1)</param>
/// <param name="action">The actual code to measure.</param>
/// <param name="reset">Code that will be called before 'action' to reset the state of the benchmark.</param>
/// <returns>A Stats object representing the measurements (in usec)</returns>
public Stats Measure(string name, float scale, Action action, Action reset)
{
if (reset != null && IterationCount != 1)
throw new ApplicationException("Reset can only be used on timers with an iteration count of 1");
Stats statsUSec = new Stats();
if (Prime)
{
if (reset != null)
reset();
action();
}
for (int i = 0; i < SampleCount; i++)
{
if (reset != null)
reset();
statsUSec.Add(timer.Measure(name, scale, action));
}
if (OnMeasure != null)
OnMeasure(name, IterationCount, scale, statsUSec);
return statsUSec;
}
/// <summary>
/// Prints the mean, median, min, max, and stdDev and count of the samples to the Console
/// Useful as a target for OnMeasure
/// </summary>
public static MeasureCallback PrintStats = delegate(string name, int iterationCount, float scale, Stats sample)
{
Console.WriteLine(name + ": " + sample.ToString());
};
/// <summary>
/// Prints the mean with a error bound (2 standard deviations, which imply a you have
/// 95% confidence that a sampleUsec will be with the bounds (for a normal distribution).
/// This is a good default target for OnMeasure.
/// </summary>
public static MeasureCallback Print = delegate(string name, int iterationCount, float scale, Stats sample)
{
// +- two standard deviations covers 95% of all samples in a normal distribution
float errorPercent = (sample.StandardDeviation * 2 * 100) / Math.Abs(sample.Mean);
string errorString = ">400%";
if (errorPercent < 400)
errorString = (errorPercent.ToString("f0") + "%").PadRight(5);
string countString = "";
if (iterationCount != 1)
countString = "count: " + iterationCount.ToString() + " ";
Console.WriteLine(name + ": " + countString + sample.Mean.ToString("f3").PadLeft(8) + " +- " + errorString + " msec");
};
#region privates
CodeTimer timer;
#endregion
};
/// <summary>
/// CodeTimer is a simple wrapper that uses System.Diagnostics.StopWatch
/// to time the body of some code (given by a delegate), to high precision.
/// </summary>
public class CodeTimer
{
public CodeTimer() : this(1) { }
public CodeTimer(int iterationCount)
{
this.iterationCount = iterationCount;
Prime = true;
// Spin the CPU for a while. This should help insure that the CPU gets out of any low power
// mode so so that we get more stable results.
// TODO: see if this is true, and if there is a better way of doing it.
Stopwatch sw = Stopwatch.StartNew();
while (sw.ElapsedMilliseconds < 32)
;
}
/// <summary>
/// The number of times the benchmark is run in a loop for a single measument.
/// </summary>
public int IterationCount
{
get { return iterationCount; }
set
{
iterationCount = value;
overheadValid = false;
}
}
/// <summary>
/// By default CodeTimer will run the action once before doing a
/// measurement run. This insures one-time actions like JIT
/// compilation are not being measured. However if the benchmark is
/// not idempotent, this can be a problem. Setting Prime=false
/// insures that this Priming does not happen.
/// </summary>
public bool Prime;
public delegate void MeasureCallback(string name, int iterationCount, float sample);
/// <summary>
/// OnMeasure is signaled every time a Measure() is called.
/// </summary>
public event MeasureCallback OnMeasure;
/// <summary>
/// The smallest time (in microseconds) that can be resolved by the timer).
/// </summary>
public static float ResolutionUsec { get { return 1000000.0F / Stopwatch.Frequency; } }
/// <summary>
/// Returns the number of microsecond it took to run 'action', 'count' times.
/// </summary>
public float Measure(string name, Action action)
{
return Measure(name, 1, action);
}
/// <summary>
/// Returns the number of microseconds it to to run action 'count' times divided by 'scale'.
/// Scaling is useful if you want to normalize to a single iteration for example.
/// </summary>
public float Measure(string name, float scale, Action action)
{
Stopwatch sw = new Stopwatch();
// Run the action once to do any JITTing that might happen.
if (Prime)
action();
float overheadUsec = GetOverheadUsec(action);
sw.Reset();
sw.Start();
for (int j = 0; j < iterationCount; j++)
action();
sw.Stop();
float sampleUsec = (float)((sw.Elapsed.TotalMilliseconds * 1000.0F - overheadUsec) / scale / iterationCount);
if (!computingOverhead && OnMeasure != null)
OnMeasure(name, iterationCount, sampleUsec);
return sampleUsec;
}
/// <summary>
/// Prints the result of a CodeTimer to standard output.
/// This is a good default target for OnMeasure.
/// </summary>
public static MeasureCallback Print = delegate(string name, int iterationCount, float sample)
{
Console.WriteLine("{0}: count={1} time={2:f3} msec ", name, iterationCount, sample);
};
#region privates
/// <summary>
/// Time the overheadUsec of the harness that does nothing so we can subtract it out.
///
/// Because calling delegates on static methods is more expensive than caling delegates on
/// instance methods we need the action to determine the overheadUsec.
/// </summary>
/// <returns></returns>
float GetOverheadUsec(Action action)
{
if (!overheadValid)
{
if (computingOverhead)
return 0.0F;
computingOverhead = true;
// Compute the overheads of calling differnet types of delegates.
Action emptyInstanceAction = new Action(this.emptyMethod);
// Prime the actions (JIT them)
Measure(null, emptyInstanceAction);
// Compute the min over 5 runs (figuring better not to go negative)
instanceOverheadUsec = float.MaxValue;
for (int i = 0; i < 5; i++)
{
// We multiply by iteration count because we don't want this scaled by the
// count but 'Measure' does it by whether we want it or not.
instanceOverheadUsec = Math.Min(Measure(null, emptyInstanceAction) * IterationCount, instanceOverheadUsec);
}
Action emptyStaticAction = new Action(emptyStaticMethod);
Measure(null, emptyStaticAction);
staticOverheadUsec = float.MaxValue;
for (int i = 0; i < 5; i++)
staticOverheadUsec = Math.Min(Measure(null, emptyStaticAction) * IterationCount, staticOverheadUsec);
computingOverhead = false;
overheadValid = true;
}
if (action.Target == null)
return staticOverheadUsec;
else
return instanceOverheadUsec;
}
static private void emptyStaticMethod() { }
private void emptyMethod() { }
bool overheadValid;
bool computingOverhead;
int iterationCount;
float staticOverheadUsec;
float instanceOverheadUsec;
#endregion
};
}