line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Dumbbench; |
2
|
3
|
|
|
3
|
|
112568
|
use strict; |
|
3
|
|
|
|
|
12
|
|
|
3
|
|
|
|
|
71
|
|
3
|
3
|
|
|
3
|
|
11
|
use warnings; |
|
3
|
|
|
|
|
4
|
|
|
3
|
|
|
|
|
60
|
|
4
|
3
|
|
|
3
|
|
10
|
use Carp (); |
|
3
|
|
|
|
|
5
|
|
|
3
|
|
|
|
|
36
|
|
5
|
3
|
|
|
3
|
|
1194
|
use Time::HiRes (); |
|
3
|
|
|
|
|
3455
|
|
|
3
|
|
|
|
|
184
|
|
6
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
our $VERSION = '0.503'; |
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
require Dumbbench::Result; |
10
|
|
|
|
|
|
|
require Dumbbench::Stats; |
11
|
|
|
|
|
|
|
require Dumbbench::Instance; |
12
|
|
|
|
|
|
|
|
13
|
3
|
|
|
3
|
|
1216
|
use Params::Util '_INSTANCE'; |
|
3
|
|
|
|
|
15229
|
|
|
3
|
|
|
|
|
219
|
|
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
use Class::XSAccessor { |
16
|
3
|
|
|
|
|
28
|
getters => [qw( |
17
|
|
|
|
|
|
|
target_rel_precision |
18
|
|
|
|
|
|
|
target_abs_precision |
19
|
|
|
|
|
|
|
initial_runs |
20
|
|
|
|
|
|
|
max_iterations |
21
|
|
|
|
|
|
|
variability_measure |
22
|
|
|
|
|
|
|
started |
23
|
|
|
|
|
|
|
outlier_rejection |
24
|
|
|
|
|
|
|
subtract_dry_run |
25
|
|
|
|
|
|
|
)], |
26
|
|
|
|
|
|
|
accessors => [qw(verbosity)], |
27
|
3
|
|
|
3
|
|
1283
|
}; |
|
3
|
|
|
|
|
5478
|
|
28
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
sub new { |
31
|
3
|
|
|
3
|
1
|
6
|
my $proto = shift; |
32
|
3
|
|
33
|
|
|
16
|
my $class = ref($proto)||$proto; |
33
|
3
|
|
|
|
|
5
|
my $self; |
34
|
3
|
50
|
|
|
|
11
|
if (not ref($proto)) { |
35
|
3
|
|
|
|
|
32
|
$self = bless { |
36
|
|
|
|
|
|
|
verbosity => 0, |
37
|
|
|
|
|
|
|
target_rel_precision => 0.05, |
38
|
|
|
|
|
|
|
target_abs_precision => 0, |
39
|
|
|
|
|
|
|
initial_runs => 20, |
40
|
|
|
|
|
|
|
max_iterations => 10000, |
41
|
|
|
|
|
|
|
variability_measure => 'mad', |
42
|
|
|
|
|
|
|
instances => [], |
43
|
|
|
|
|
|
|
started => 0, |
44
|
|
|
|
|
|
|
outlier_rejection => 3, |
45
|
|
|
|
|
|
|
subtract_dry_run => 1, |
46
|
|
|
|
|
|
|
@_, |
47
|
|
|
|
|
|
|
} => $class; |
48
|
|
|
|
|
|
|
} |
49
|
|
|
|
|
|
|
else { |
50
|
0
|
|
|
|
|
0
|
$self = bless {%$proto, @_} => $class; |
51
|
0
|
|
|
|
|
0
|
my @inst = $self->instances; |
52
|
0
|
|
|
|
|
0
|
$self->{instances} = []; |
53
|
0
|
|
|
|
|
0
|
foreach my $instance (@inst) { |
54
|
0
|
|
|
|
|
0
|
push @{$self->{instances}}, $instance->new; |
|
0
|
|
|
|
|
0
|
|
55
|
|
|
|
|
|
|
} |
56
|
|
|
|
|
|
|
} |
57
|
|
|
|
|
|
|
|
58
|
3
|
50
|
33
|
|
|
36
|
if ($self->target_abs_precision <= 0 and $self->target_rel_precision <= 0) { |
59
|
0
|
|
|
|
|
0
|
Carp::croak("Need either target_rel_precision or target_abs_precision > 0"); |
60
|
|
|
|
|
|
|
} |
61
|
3
|
50
|
|
|
|
12
|
if ($self->initial_runs < 6) { |
62
|
3
|
|
|
|
|
963
|
Carp::carp("Number of initial runs is very small (<6). Precision will be off."); |
63
|
|
|
|
|
|
|
} |
64
|
|
|
|
|
|
|
|
65
|
3
|
|
|
|
|
102
|
return $self; |
66
|
|
|
|
|
|
|
} |
67
|
|
|
|
|
|
|
|
68
|
|
|
|
|
|
|
sub add_instances { |
69
|
4
|
|
|
4
|
1
|
5
|
my $self = shift; |
70
|
|
|
|
|
|
|
|
71
|
4
|
50
|
|
|
|
59
|
if ($self->started) { |
72
|
0
|
|
|
|
|
0
|
Carp::croak("Can't add instances after the benchmark has been started"); |
73
|
|
|
|
|
|
|
} |
74
|
4
|
|
|
|
|
10
|
foreach my $instance (@_) { |
75
|
4
|
50
|
|
|
|
47
|
if (not _INSTANCE($instance, 'Dumbbench::Instance')) { |
76
|
0
|
|
|
|
|
0
|
Carp::croak("Argument to add_instances is not a Dumbbench::Instance"); |
77
|
|
|
|
|
|
|
} |
78
|
|
|
|
|
|
|
} |
79
|
4
|
|
|
|
|
9
|
push @{$self->{instances}}, @_; |
|
4
|
|
|
|
|
11
|
|
80
|
|
|
|
|
|
|
} |
81
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
sub instances { |
83
|
10
|
|
|
10
|
1
|
18
|
my $self = shift; |
84
|
10
|
|
|
|
|
13
|
return @{$self->{instances}}; |
|
10
|
|
|
|
|
137
|
|
85
|
|
|
|
|
|
|
} |
86
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
sub run { |
88
|
3
|
|
|
3
|
1
|
6
|
my $self = shift; |
89
|
3
|
50
|
|
|
|
8
|
Carp::croak("Can't re-run same benchmark instance") if $self->started; |
90
|
3
|
50
|
|
|
|
16
|
$self->dry_run_timings if $self->subtract_dry_run; |
91
|
3
|
|
|
|
|
29
|
$self->run_timings; |
92
|
|
|
|
|
|
|
} |
93
|
|
|
|
|
|
|
|
94
|
|
|
|
|
|
|
sub run_timings { |
95
|
3
|
|
|
3
|
0
|
13
|
my $self = shift; |
96
|
3
|
|
|
|
|
5
|
$self->{started} = 1; |
97
|
3
|
|
|
|
|
12
|
foreach my $instance ($self->instances) { |
98
|
4
|
50
|
|
|
|
22
|
next if $instance->result; |
99
|
4
|
|
|
|
|
9
|
$self->_run($instance); |
100
|
|
|
|
|
|
|
} |
101
|
|
|
|
|
|
|
} |
102
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
sub dry_run_timings { |
104
|
3
|
|
|
3
|
0
|
6
|
my $self = shift; |
105
|
3
|
|
|
|
|
6
|
$self->{started} = 1; |
106
|
|
|
|
|
|
|
|
107
|
3
|
|
|
|
|
12
|
foreach my $instance ($self->instances) { |
108
|
4
|
50
|
|
|
|
20
|
next if $instance->dry_result; |
109
|
4
|
|
|
|
|
16
|
$self->_run($instance, 'dry'); |
110
|
|
|
|
|
|
|
} |
111
|
|
|
|
|
|
|
} |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
sub _run { |
114
|
8
|
|
|
8
|
|
14
|
my $self = shift; |
115
|
8
|
|
|
|
|
13
|
my $instance = shift; |
116
|
8
|
|
|
|
|
13
|
my $dry = shift; |
117
|
|
|
|
|
|
|
|
118
|
8
|
|
|
|
|
37
|
my $name = $instance->_name_prefix; |
119
|
|
|
|
|
|
|
|
120
|
|
|
|
|
|
|
# for overriding in case of dry-run mode |
121
|
8
|
|
50
|
|
|
38
|
my $V = $self->verbosity || 0; |
122
|
8
|
|
|
|
|
20
|
my $initial_timings = $self->initial_runs; |
123
|
8
|
|
|
|
|
18
|
my $abs_precision = $self->target_abs_precision; |
124
|
8
|
|
|
|
|
18
|
my $rel_precision = $self->target_rel_precision; |
125
|
8
|
|
|
|
|
15
|
my $max_iterations = $self->max_iterations; |
126
|
|
|
|
|
|
|
|
127
|
8
|
100
|
|
|
|
17
|
if ($dry) { |
128
|
4
|
50
|
|
|
|
6
|
$V--; $V = 0 if $V < 0; |
|
4
|
|
|
|
|
9
|
|
129
|
4
|
|
|
|
|
8
|
$initial_timings *= 5; |
130
|
4
|
|
|
|
|
5
|
$abs_precision = 0; |
131
|
4
|
|
|
|
|
9
|
$rel_precision /= 2; |
132
|
4
|
|
|
|
|
4
|
$max_iterations *= 10; |
133
|
|
|
|
|
|
|
} |
134
|
|
|
|
|
|
|
|
135
|
8
|
50
|
|
|
|
19
|
print "${name}Running initial timing for warming up the cache...\n" if $V; |
136
|
8
|
100
|
|
|
|
16
|
if ($dry) { |
137
|
|
|
|
|
|
|
# be generous, this is fast |
138
|
4
|
|
|
|
|
23
|
$instance->single_dry_run() for 1..3; |
139
|
|
|
|
|
|
|
} |
140
|
|
|
|
|
|
|
else { |
141
|
4
|
|
|
|
|
14
|
$instance->single_run(); |
142
|
|
|
|
|
|
|
} |
143
|
|
|
|
|
|
|
|
144
|
8
|
|
|
|
|
13
|
my @timings; |
145
|
8
|
50
|
|
|
|
19
|
print "${name}Running $initial_timings initial timings...\n" if $V; |
146
|
8
|
|
|
|
|
15
|
foreach (1..$initial_timings) { |
147
|
24
|
50
|
|
|
|
40
|
print "${name}Running timing $_...\n" if $V > 1; |
148
|
24
|
100
|
|
|
|
48
|
push @timings, ($dry ? $instance->single_dry_run() : $instance->single_run()); |
149
|
|
|
|
|
|
|
} |
150
|
|
|
|
|
|
|
|
151
|
8
|
50
|
|
|
|
18
|
print "${name}Iterating until target precision reached...\n" if $V; |
152
|
|
|
|
|
|
|
|
153
|
8
|
|
|
|
|
54
|
my $stats = Dumbbench::Stats->new(data => \@timings); |
154
|
8
|
|
|
|
|
14
|
my $sigma; |
155
|
|
|
|
|
|
|
my $mean; |
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
#My mental model for the distribution was Gauss+outliers. |
158
|
|
|
|
|
|
|
#If my expectation is correct, the following algorithm should produce a reasonable EV +/- uncertainty: |
159
|
|
|
|
|
|
|
#1) Calc. median of the whole distribution. |
160
|
|
|
|
|
|
|
#2) Calculate the median-absolute deviation from the whole distribution (MAD, see wikipedia). It needs rescaling to become a measure of variability that is robust against outliers. |
161
|
|
|
|
|
|
|
#(The MAD will be our initial guess for a "sigma") |
162
|
|
|
|
|
|
|
#3) Reject the samples that are outside $median +/- $n*$MAD. |
163
|
|
|
|
|
|
|
#I was expecting several high outliers but few lows. An ordinary truncated mean or the like would be unsuitable for removing the outliers in such a case since you'd get a significant upward bias of your EV. |
164
|
|
|
|
|
|
|
#By using the median as the initial guess, we keep the initial bias to a minimum. The MAD will be similarly unaffected by outliers AND the asymmetry. |
165
|
|
|
|
|
|
|
#Thus cutting the tails won't blow up the bias too strongly (hopefully). |
166
|
|
|
|
|
|
|
#4) Calculate mean & MAD/sqrt($n) of the remaining distribution. These are our EV and uncertainty on the mean. |
167
|
|
|
|
|
|
|
|
168
|
8
|
|
|
|
|
12
|
my $n_good = 0; |
169
|
8
|
|
|
|
|
22
|
my $variability_measure = $self->variability_measure; |
170
|
8
|
|
|
|
|
12
|
while (1) { |
171
|
12
|
|
|
|
|
48
|
my ($good, $outliers) = $stats->filter_outliers( |
172
|
|
|
|
|
|
|
variability_measure => $variability_measure, |
173
|
|
|
|
|
|
|
nsigma_outliers => $self->outlier_rejection, |
174
|
|
|
|
|
|
|
); |
175
|
|
|
|
|
|
|
|
176
|
12
|
|
|
|
|
19
|
$n_good = @$good; |
177
|
|
|
|
|
|
|
|
178
|
12
|
50
|
33
|
|
|
27
|
if (not $n_good and @timings >= $max_iterations) { |
179
|
0
|
|
|
|
|
0
|
$mean = 0; $sigma = 0; |
|
0
|
|
|
|
|
0
|
|
180
|
0
|
|
|
|
|
0
|
last; |
181
|
|
|
|
|
|
|
} |
182
|
|
|
|
|
|
|
|
183
|
12
|
50
|
|
|
|
34
|
if ($n_good) { |
184
|
12
|
|
|
|
|
25
|
my $new_stats = Dumbbench::Stats->new(data => $good); |
185
|
12
|
|
|
|
|
27
|
$sigma = $new_stats->$variability_measure() / sqrt($n_good); |
186
|
12
|
|
|
|
|
87
|
$mean = $new_stats->mean(); |
187
|
|
|
|
|
|
|
|
188
|
|
|
|
|
|
|
# stop condition |
189
|
12
|
|
|
|
|
14
|
my $need_iter = 0; |
190
|
12
|
50
|
|
|
|
28
|
if ($rel_precision > 0) { |
191
|
12
|
|
|
|
|
19
|
my $rel = $sigma/$mean; |
192
|
12
|
50
|
|
|
|
18
|
print "${name}Reached relative precision $rel (neeed $rel_precision).\n" if $V > 1; |
193
|
12
|
50
|
|
|
|
23
|
$need_iter++ if $rel > $rel_precision; |
194
|
|
|
|
|
|
|
} |
195
|
12
|
50
|
|
|
|
20
|
if ($abs_precision > 0) { |
196
|
0
|
0
|
|
|
|
0
|
print "${name}Reached absolute precision $sigma (neeed $abs_precision).\n" if $V > 1; |
197
|
0
|
0
|
|
|
|
0
|
$need_iter++ if $sigma > $abs_precision; |
198
|
|
|
|
|
|
|
} |
199
|
12
|
100
|
|
|
|
18
|
if ($n_good < $initial_timings) { |
200
|
4
|
|
|
|
|
6
|
$need_iter++; |
201
|
|
|
|
|
|
|
} |
202
|
12
|
100
|
66
|
|
|
40
|
last if not $need_iter or @timings >= $max_iterations; |
203
|
|
|
|
|
|
|
} |
204
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
# progressively run more new timings in one go. Otherwise, |
206
|
|
|
|
|
|
|
# we start to stall on the O(n*log(n)) complexity of the median. |
207
|
4
|
|
|
|
|
21
|
my $n = List::Util::min( $max_iterations - @timings, List::Util::max(1, @timings*0.05) ); |
208
|
4
|
50
|
|
|
|
27
|
push @timings, ($dry ? $instance->single_dry_run() : $instance->single_run()) for 1..$n; |
209
|
|
|
|
|
|
|
} # end while more data required |
210
|
|
|
|
|
|
|
|
211
|
8
|
50
|
33
|
|
|
21
|
if (@timings >= $max_iterations and not $dry) { |
212
|
0
|
|
|
|
|
0
|
print "${name}Reached maximum number of iterations. Stopping. Precision not reached.\n"; |
213
|
|
|
|
|
|
|
} |
214
|
|
|
|
|
|
|
|
215
|
|
|
|
|
|
|
# rescale sigma |
216
|
|
|
|
|
|
|
# This is necessary since by cutting everything outside of n-sigma, |
217
|
|
|
|
|
|
|
# we artificially reduce the variability of the main distribution. |
218
|
8
|
50
|
|
|
|
23
|
if ($self->outlier_rejection) { |
219
|
|
|
|
|
|
|
# TODO implement |
220
|
|
|
|
|
|
|
} |
221
|
|
|
|
|
|
|
|
222
|
8
|
|
|
|
|
46
|
my $result = Dumbbench::Result->new( |
223
|
|
|
|
|
|
|
timing => $mean, |
224
|
|
|
|
|
|
|
uncertainty => $sigma, |
225
|
|
|
|
|
|
|
nsamples => $n_good, |
226
|
|
|
|
|
|
|
); |
227
|
|
|
|
|
|
|
|
228
|
8
|
100
|
|
|
|
20
|
if ($dry) { |
229
|
4
|
|
|
|
|
12
|
$instance->{dry_timings} = \@timings; |
230
|
4
|
|
|
|
|
23
|
$instance->dry_result($result); |
231
|
|
|
|
|
|
|
} |
232
|
|
|
|
|
|
|
else { |
233
|
4
|
|
|
|
|
8
|
$instance->{timings} = \@timings; |
234
|
4
|
50
|
33
|
|
|
37
|
$result -= $instance->dry_result |
235
|
|
|
|
|
|
|
if defined $instance->dry_result and $self->subtract_dry_run; |
236
|
4
|
|
|
|
|
507
|
$instance->result($result); |
237
|
|
|
|
|
|
|
} |
238
|
|
|
|
|
|
|
} |
239
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
sub report { |
241
|
0
|
|
|
0
|
1
|
|
my ( $self, $raw, $options ) = @_; |
242
|
0
|
|
0
|
|
|
|
$options ||= {}; |
243
|
0
|
0
|
|
|
|
|
Carp::carp( "The second option to report was not a hash ref" ) |
244
|
|
|
|
|
|
|
unless ref $options eq ref {}; |
245
|
|
|
|
|
|
|
|
246
|
0
|
|
|
|
|
|
foreach my $instance ($self->instances) { |
247
|
0
|
|
|
|
|
|
my $result = $instance->result; |
248
|
0
|
0
|
|
|
|
|
my $result_str = ($options->{float}) ? unscientific_notation($result) : "$result"; |
249
|
|
|
|
|
|
|
|
250
|
0
|
0
|
|
|
|
|
if (not $raw) { |
251
|
0
|
|
|
|
|
|
my $mean = $result->raw_number; |
252
|
0
|
|
|
|
|
|
my $sigma = $result->raw_error->[0]; |
253
|
0
|
|
|
|
|
|
my $name = $instance->_name_prefix; |
254
|
|
|
|
|
|
|
printf( |
255
|
|
|
|
|
|
|
"%sRan %u iterations (%u outliers).\n", |
256
|
|
|
|
|
|
|
$name, |
257
|
0
|
|
|
|
|
|
scalar(@{$instance->timings}), |
258
|
0
|
|
|
|
|
|
scalar(@{$instance->timings})-$result->nsamples |
|
0
|
|
|
|
|
|
|
259
|
|
|
|
|
|
|
); |
260
|
0
|
|
|
|
|
|
printf( |
261
|
|
|
|
|
|
|
"%sRounded run time per iteration (seconds): %s (%.1f%%)\n", |
262
|
|
|
|
|
|
|
$name, |
263
|
|
|
|
|
|
|
$result_str, |
264
|
|
|
|
|
|
|
$sigma/$mean*100 |
265
|
|
|
|
|
|
|
); |
266
|
0
|
0
|
|
|
|
|
if ($self->verbosity) { |
267
|
0
|
|
|
|
|
|
printf("%sRaw: $mean +/- $sigma\n", $name); |
268
|
|
|
|
|
|
|
} |
269
|
|
|
|
|
|
|
} |
270
|
|
|
|
|
|
|
else { |
271
|
0
|
|
|
|
|
|
print $result_str, "\n"; |
272
|
|
|
|
|
|
|
} |
273
|
|
|
|
|
|
|
} |
274
|
|
|
|
|
|
|
} |
275
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
sub box_plot { |
277
|
0
|
|
|
0
|
1
|
|
my $self = shift; |
278
|
0
|
|
|
|
|
|
require Dumbbench::BoxPlot; |
279
|
|
|
|
|
|
|
|
280
|
0
|
|
|
|
|
|
return Dumbbench::BoxPlot->new($self); |
281
|
|
|
|
|
|
|
} |
282
|
|
|
|
|
|
|
|
283
|
|
|
|
|
|
|
sub unscientific_notation { |
284
|
0
|
|
|
0
|
0
|
|
sprintf( "%f %s %f", split( / /, $_[0] ) ); |
285
|
|
|
|
|
|
|
} |
286
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
1; |
288
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
__END__ |