# Benchmark of Elementary Mathematical Operations in Node.js How fast or slow are common mathematical operations to execute in a web browser or Node.js? How slow is a division when compared to a multiplication or a sine function compared to a square root? This article aims to find rough estimates for various elementary operations to aid algorithmic design and implementations that prioritise computational efficiency.

The reader should note that computation times vary between programming languages, interpreters, compilers, and devices. The surrounding code matters too, because the compilers and interpreters try to optimise the code in many complex ways . Also, often a designer of an algorithm is more interested on the computational complexity of the algorithm, referring to the effect of the input size in respect to execution time and memory usage . On the other hand, in situations where a piece of code is executed thousands or millions of times per second, knowledge on costs of the basic operations becomes handy.

Similar benchmarks have been conducted earlier by Lincoln Atkinson  and Erik Wernersson  in C++ programming language. Here we attempt to replicate the benchmarks in JavaScript and then compare the results.

## Results

Our benchmark source code is released as a GitHub repository . See the repository for implementation details. The details of the used benchmark environment are given later in this article. Below are the results after running the benchmark three times:

``` Operation           Run 1    Run 2    Run 3
+-------------------+--------+--------+--------+
Baseline
x => 0              0.27 ms  0.26 ms  0.25 ms
x => x              0.32 ms  0.31 ms  0.30 ms
x => 3 + x          1.03 ms  1.02 ms  1.00 ms
x => 3 - n          1.02 ms  1.05 ms  0.99 ms
x => -x             1.04 ms  1.03 ms  0.98 ms
Multiplication
x => 3 * x          1.03 ms  1.02 ms  1.00 ms
x => x * x          1.04 ms  1.02 ms  0.97 ms
x => 3 / n          1.04 ms  1.04 ms  1.01 ms
Exponentiation
x => Math.sqrt(x)   1.13 ms  1.09 ms  1.07 ms
x => Math.exp(x)    2.06 ms  2.05 ms  2.02 ms
x => Math.pow(x,3)  2.83 ms  2.87 ms  2.84 ms
Logarithms
x => Math.log(x)    1.89 ms  1.94 ms  1.86 ms
x => Math.log2(x)   1.91 ms  1.91 ms  1.87 ms
x => Math.log10(x)  1.91 ms  1.97 ms  1.91 ms
Rounding
x => Math.ceil(x)   1.00 ms  0.96 ms  0.96 ms
x => Math.floor(x)  0.95 ms  0.94 ms  0.92 ms
x => Math.round(x)  1.05 ms  1.06 ms  1.02 ms
Trigonometry
x => Math.sin(x)    2.09 ms  2.14 ms  2.08 ms
x => Math.cos(x)    2.10 ms  2.12 ms  2.08 ms
x => Math.asin(x)   1.66 ms  1.71 ms  1.62 ms
x => Math.sinh(x)   2.18 ms  2.20 ms  2.13 ms
x => Math.tan(x)    2.28 ms  2.35 ms  2.25 ms
x => Math.atan(x)   2.00 ms  2.00 ms  1.98 ms```

During a single run, each operation is executed around 500 times for an array of 10000 random numbers. The numbers in the array are floats within the range -100..100. The reported times tell how much the mapping of the 10000 elements took in average.

To analyse the raw results, let us compare the groups. Addition, multiplication, and rounding took roughly equal time. Logarithms and trigonometric functions took roughly twice as much time. Surprisingly the exponentiation group of operations `sqrt`, `exp`, and `pow` was more divided. The square root was almost on a par with multiplication and e to the power of x on a par with logarithms. Taking x to the power of 3 was the slowest operation of the benchmark and took almost three times longer than multiplication and division.

Let us then assume the baseline `x => x` captures the time required for the things other that the actual operation, like variable declaration and memory management. Therefore, to get a better relative comparison between the operations, we should first normalise their times by subtracting the baseline. For that, let us use a relative unit of time, U, instead of actual milliseconds. In our case, 1 ms ~ 3 U and the baseline is 1 U. For example, the addition took ~1 ms = 3 U and after the baseline normalisation its run time becomes 2 U.

Now we summarise the results by normalising and sorting them in ascending order:
■■ addition, multiplication, and division took 2 U
■■ ceil, floor, and round: 2 U
■■ square root: 2 U
■■■■ asin: 4 U
■■■■■ exp and logarithms: 5 U
■■■■■ sin, cos, atan, sinh: 5 U
■■■■■■ tan: 6 U
■■■■■■■■ pow: 8 U

The environment used for the benchmark was:
– Mac mini (Late 2012)
– 2,5 GHz Intel Core i5
– macOS Mojave 10.14.6
– Node.js 14.5.0

We understand that to get general and reliable results, the benchmark should be averaged over various environments. Lacking resources to do so, we settle to comparing them to a couple of earlier benchmarks.

## Comparison with earlier benchmarks

Let us summarise the earlier benchmarks by Atkinson  and Wernersson  and then compare them to our results above. Because the benchmark results are relative to the processing speed and the number of iterations, we again use the relative a unit of time, U, and note that it is only comparable within the same benchmark.

Our rough summary of the simple C++ benchmark by Atkinson:
addition and multiplication took 1 U
■■■■ division: 4 U
■■■■■■ square root: 6 U
■■■■■■■■■ exp: 9 U
■■■■■■■■■■■■■■ sin, cos: 14 U
■■■■■■■■■■■■■■■■■■■ tan: 19 U
■■■■■■■■■■■■■■■■■■■■■■■ atan: 23 U

Our rough summary of the broader C++ benchmark by Wernersson:
floor, ceil, addition, multiplication, and division took 1 U
■■ square root: 2 U
■■■■■■■■ round: 8 U
■■■■■■■■■■■■■■ exp: 14 U
■■■■■■■■■■■■■■■ sin: 15 U
■■■■■■■■■■■■■■■■■■■ asin, sinh: 19 U
■■■■■■■■■■■■■■■■■■■■■■ atan, cube root: 22 U
■■■■■■■■■■■■■■■■■■■■■■■■■ tan: 25 U
■■■■■■■■■■■■■■■■■■■■■■■■■■ log, log10: 26 U
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ pow: 62 U

By comparing the C++ benchmarks to our Node.js benchmark, we can conclude that the general order of the operations is mostly the same. Addition, multiplication, and division are the cheapest of the operations. Then comes square root and exp, followed by logarithms and trigonometric functions, and finally the most expensive of them all, pow.

The main notable difference between benchmarks is the magnitude of differences. Where the trigonometric functions took only thrice the time of the multiplication in Node.js, the factor was almost 20 in C++. This is likely due to overhead caused by the JavaScript interpreter, diminishing the share of clock cycles used for actual number crunching. Complied C++ deals closely with the hardware layer and is thus able to use the cycles more efficiently for the simplest arithmetics.

## Conclusion

In this article we compared about twenty common math operations in JavaScript. We found that there are significant differences between their run times, although the differences are small when contrasted with C++ benchmarks. Although the real run times may vary across devices and programs, the results are in line with the earlier studies and thus work as a rough guide.

To end with a few subjective remarks, we were surprised by the high cost of taking x to the power of n. No wonder why `pow(x, 2)` is often replaced by `x * x`. We were also surprised by relative cheapness of `sqrt`. Before the benchmark we assumed it to be on a par with `sin` and `cos`.

We hope the benchmark and its results gave you a basic understanding on the relative costs of various elementary operations in JavaScript and C++. If you spot any errors or like to share your thoughts or additional results, feel free to comment below. Thank you.

## References

 B. Read. JavaScript: Microbenchmarks and their pitfalls. 2020.
 Wikipedia. Computational complexity. 2021.
 L. Atkinson. A simple benchmark of various math operations. 2014.
 E. Wernersson. math_ops_speed: testing speed of mathematical operators. GitHub repository. 2020.
 A. Palén. A Benchmark for basic JavaScript math operations. GitHub repository. 2021. 