File Coverage

blib/lib/Dumbbench.pm
Criterion Covered Total %
statement 108 144 75.0
branch 36 72 50.0
condition 8 22 36.3
subroutine 13 16 81.2
pod 6 9 66.6
total 171 263 65.0


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__