-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathBench.php
More file actions
138 lines (130 loc) · 4.88 KB
/
Bench.php
File metadata and controls
138 lines (130 loc) · 4.88 KB
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
<?php
declare(strict_types=1);
namespace Testo;
use Testo\Bench\Internal\Pipeline\BenchInterceptor;
use Testo\Pipeline\Attribute\FallbackInterceptor;
use Testo\Pipeline\Attribute\Interceptable;
/**
* Marks a method or function as a benchmark subject and compares its performance against
* the provided callables.
*
* The marked method (or function) automatically receives the alias `current`.
* All callables listed in {@see Bench::$callables} are benchmarked under the same conditions.
* Results are ranked by filtered average time — the fastest callable takes first place
* and serves as the baseline for relative comparison.
*
* ## How it works
*
* 1. Each callable is invoked `$calls` times per iteration (to reduce measurement noise).
* 2. This is repeated for `$iterations` iterations (to collect statistical data).
* 3. Outliers are detected and filtered using the MAD (Median Absolute Deviation) method.
* 4. Results are ranked by filtered average time — the fastest callable wins.
*
* ## Aliases
*
* - The marked method always has the alias `current`.
* - For other callables, use **string keys** in the `$callables` array to assign aliases.
* These aliases appear in the results table for easy identification.
*
* ## Usage examples
*
* Basic — compare the current method against one alternative:
*
* ```
* #[Bench(['array_sum' => [self::class, 'sumWithArraySum']])]
* public static function sumWithLoop(int $a, int $b): int { ... }
* ```
*
* With arguments and tuning parameters:
*
* ```
* #[Bench(
* [
* 'shift' => [self::class, 'secondImpl'],
* 'multi' => [self::class, 'thirdImpl'],
* ],
* arguments: [1, 20_000],
* calls: 100_000,
* iterations: 10,
* )]
* public static function firstImpl(int $a, int $b): int { ... }
* ```
*
* The attribute is repeatable — you can attach multiple `#[BenchWith]` to a single method
* to run independent benchmark scenarios with different callables or arguments.
*
* ## Stability
*
* Results are considered stable when the relative standard deviation (RStDev) is below 2%.
* If variance is too high, Testo will generate diagnostic reports with actionable advice
* (e.g., increase `$calls` or `$iterations`).
*
* @link https://php-testo.github.io/blog/collider.md
*
* @api
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION | \Attribute::IS_REPEATABLE)]
#[FallbackInterceptor(BenchInterceptor::class)]
final readonly class Bench implements Interceptable
{
public function __construct(
/**
* Functions to benchmark against the current (marked) method.
*
* Each entry can be:
* - A callable (closure, function name, `[object, method]`, etc.)
* - An array `[class-string, non-empty-string]` to reference a non-public method by class and name.
*
* Use **string keys** to assign aliases that will appear in the results table:
*
* ```
* ['shift' => [self::class, 'altImpl'], 'naive' => 'str_replace']
* ```
*
* Numeric keys are converted to string and used as-is.
*
* @var array<callable|array{class-string, non-empty-string}>
*/
public array $callables,
/**
* Arguments passed to every benchmarked function (including `current`).
*
* All functions receive the same arguments to ensure a fair comparison.
*
* @var array
*/
public array $arguments = [],
/**
* Number of warmup calls before the actual benchmark iterations.
*
* Warmup runs eliminate cold-start overhead (lazy initialization, first-call allocations, etc.).
* Results from warmup calls are discarded.
*
* @var int<0, max>
*/
public int $warmup = 1,
/**
* Number of function calls per iteration.
*
* Higher values reduce the impact of measurement overhead and produce more stable results.
* If RStDev is too high, try increasing this value.
*
* @var int<1, max>
*/
public int $calls = 1_000,
/**
* Number of iterations (rounds).
*
* Each iteration runs all functions {@see $calls} times and records timing and memory data.
* More iterations improve statistical reliability and outlier detection.
*
* @var int<1, max>
*/
public int $iterations = 10,
) {
$warmup >= 0 or throw new \InvalidArgumentException('Warmup must be greater than or equal to 0.');
\count($callables) >= 1 or throw new \InvalidArgumentException('At least one callable must be provided.');
$calls > 0 or throw new \InvalidArgumentException('Calls must be greater than 0.');
$iterations > 0 or throw new \InvalidArgumentException('Iterations must be greater than 0.');
}
}