Here’s on Stackoverflow I often benchmark JS solutions in my answers and people ask how they could write own benchmarks. So this post is rather an answer due to the convenient code snippet tool, which the benchmark could be injected directly into. So please don’t downvote or close, just skip if not interested, any updates could go as answers. Ask your questions as comments.
The home page: https://github.com/silentmantra/benchmark
For examples you could check https://stackoverflow.com/search?tab=newest&q=user%3a14098260%20benchmark&searchOn=3
Code for benchmarking should be included in <script> tag inline. Then you should load the benchmark tool’s code, you do this either by
- With
<script>. Make sure it’s loaded after your <script> with benchmarks (You can defer (with type="module" also)):
<script src="https://cdn.jsdelivr.net/gh/silentmantra/benchmark/loader.js"></script>
- Directly inside your benchmark code in any place (usually as the last line):
/*@skip*/ fetch('https://cdn.jsdelivr.net/gh/silentmantra/benchmark/loader.js').then(r => r.text().then(eval));
As you see with /* @skip */ you can exclude a line of code from benchmarked code but execute it initially in <script>.
After loading the tool transform your <script> in an UI to show results of your benchmarks and to execute them. The results are displayed as JSON and cut to the first 500 characters or so.
A simple benchmark could be
- Some code to execute before each benchmark
- One or several solutions beginning with
// @benchmark Solution 1
where everything following @benchmark is the solution’s name.
- The execution result of a solution is the result of the last line (execution is done with
eval()).
const input = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua';
// @benchmark Array::reverse()
input.split('').reverse().join('');
// @benchmark string concat
let result = '';
for(let i = input.length - 1; i >= 0; i--){
result += input[i];
}
result;
/*@skip*/ fetch('https://cdn.jsdelivr.net/gh/silentmantra/benchmark/loader.js').then(r => r.text().then(eval));
After clicking RUN the tool estimates how many cycles to run each solution so the average time for a solution would be not less than 100ms and runs each solution 5 times by default (you can change it with the times number input). After running you can copy the results as a markup. So the above benchmark gives:
` Chrome/122
------------------------------------------------------
string concat 1.00x | x1000000 399 404 422 435 441
Array::reverse() 3.08x | x100000 123 124 130 131 139
------------------------------------------------------
https://github.com/silentmantra/benchmark `
The first solution is the fastest which is marked as 1x. The other solutions are marked how they are slower than the top solution. The next column is how many cycles the solution was run, and the next 5 (number of times the solution was run) are milliseconds taken to run the cycles with the minimum time as the first (sorted asc). Basically based on my experience the minimum time is the most presentable value of how good a solution could perform.
You can set the times and cycles manually (sometimes for heavy solutions you could prefer manually setting the cycles to 1):
const TIMES = 3, CYCLES = 1000000;
const input = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua';
// @benchmark Array::reverse()
input.split('').reverse().join('');
// @benchmark string concat
let result = '';
for(let i = input.length - 1; i >= 0; i--){
result += input[i];
}
result;
/*@skip*/ fetch('https://cdn.jsdelivr.net/gh/silentmantra/benchmark/loader.js').then(r => r.text().then(eval));
Algorithms have different time complexity so it’s possible to benchmark solutions with different input size. For that we have 3 variables:
$chunk – a chunk from which the input is composed. Could be a string, an array or a function returning either of them.
$input – an array or a string to fill with chunks.
$chunks – an array of numbers of chunks to run (default [1, 10, 100, 1000] – no need to define).
Let’s check with a string:
const $chunk = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua';
let $input = '';
// @benchmark count stupid
{
const chars = {};
for(let i = 0; i < $input.length; i++){
const char = $input[i];
chars[char] ??= $input.split('').filter(c => c === char).length;
}
chars;
}
// @benchmark count smart
{
const chars = {};
for(let i = 0; i < $input.length; i++){
const char = $input[i];
chars[char] ??= 0;
chars[char]++;
}
chars;
}
/*@skip*/ fetch('https://cdn.jsdelivr.net/gh/silentmantra/benchmark/loader.js').then(r => r.text().then(eval));
` Chrome/122
--------------------------------------------------------------------------------
> n=122 | n=1220 | n=12200 | n=122000
count smart 1.00x x100k 284 | 1.00x x10k 183 | 1.00x x1k 186 | 1.00x x100 186
count stupid 2.56x x100k 727 | 3.33x x10k 609 | 3.40x x1k 633 | 7.26x x10 135
--------------------------------------------------------------------------------
https://github.com/silentmantra/benchmark `
A top solution is the best with the most input size.
Note that you could use {} block to create a block scope to run a solution in it to avoid variable clashing.
With a chunk function:
const $chunks = [100, 1000, 10000, 100000];
const $chunk = () => [Math.random()];
const $input = [];
// @benchmark count stupid
{
const chars = {};
const allNums = $input.map(n => n.toString().slice(2)).join('');
for(let i = 0; i < allNums.length; i++){
const char = allNums[i];
chars[char] ??= allNums.split('').filter(c => c === char).length;
}
chars;
}
// @benchmark count smart
{
const chars = {};
$input.forEach(num => {
for(const char of num.toString().slice(2)){
chars[char] ??= 0;
chars[char]++;
}
});
chars;
}
/*@skip*/ fetch('https://cdn.jsdelivr.net/gh/silentmantra/benchmark/loader.js').then(r => r.text().then(eval));
` Chrome/122
------------------------------------------------------------------------------
> n=100 | n=1000 | n=10000 | n=100000
count smart 1.00x x10k 172 | 1.00x x1k 191 | 1.00x x100 246 | 1.00x x10 337
count stupid 4.19x x10k 720 | 4.01x x1k 766 | 5.53x x10 136 | 5.43x x1 183
------------------------------------------------------------------------------
https://github.com/silentmantra/benchmark `
If you need some code executed before the cycles, use // @run. For example you could introduce a set of functions/classes used in a solution (initializing them in each cycle is expensive and skews results).
const $chunks = [100, 1000, 10000, 100000];
const $chunk = () => [Math.random()];
const $input = [];
// @benchmark count stupid
{
const countChars = $input => {
const chars = {};
const allNums = $input.map(n => n.toString().slice(2)).join('');
for(let i = 0; i < allNums.length; i++){
const char = allNums[i];
chars[char] ??= allNums.split('').filter(c => c === char).length;
}
return chars;
};
// @run
countChars($input);
}
// @benchmark count smart
{
const countChars = $input => {
const chars = {};
$input.forEach(num => {
for(const char of num.toString().slice(2)){
chars[char] ??= 0;
chars[char]++;
}
});
return chars;
};
// @run
countChars($input);
}
/*@skip*/ fetch('https://cdn.jsdelivr.net/gh/silentmantra/benchmark/loader.js').then(r => r.text().then(eval));