About mutable methods of a Math object in JavaScript

Today we are publishing a translation of an article on mathematical calculations in JavaScript, which is a written version of its author’s presentation on WaffleJS . And this statement itself was a bit of a continuation of this conversation on Twitter.


Math education

Interest in math


It all started with my mathematical education, with the receipt of which I delayed somewhat. When I studied computer science, I could communicate with world-class mathematics teachers, but I missed this opportunity. I did not like math: the topics studied were very far from practice. In addition, I was already out of balance by a deeply theoretical program of computer training. I then believed that she was divorced from life, and, for the most part, I continue to think so.

But now, a few years after I completed my studies, a thirst for learning mathematics woke up in me. I was inspired by what effect even a small application of mathematical knowledge can have on my work and on my hobby. But I did not have a clear study plan.

Then, in 2012, I found a way to study math by starting work on the Simple Statistics project . Since then I have expanded and supported this project. Now it includes implementations of many algorithms; it turned out to be one of the most “ starry ” mathematical JavaScript libraries. And, apparently, people really use this library.

But I started work in 2012. If we talk about how technology has changed over this time, then it was a very, very long time ago. Since then, 8 LTS releases of Node.js have been released . Since then, JavaSript itself and the environments in which programs written in this language work have changed a lot. In 2012, the React library did not yet exist, then the first commit to the Babel project was not yet made.


The passage of time

Over the years, I noticed that my tests crashed when updating Node.js. For example, I might have something like this test:

t.equal(ss.gamma(11.54), 13098426.039156161);

This test works fine in Node.js v10, but breaks in Node.js v12. And here it’s not some super-complicated method that is tested: the function is gammaimplemented using standard JavaScript functions - Math.pow, Math.sqrtand Math.sin.

Arithmetic


I know what you can think of here: arithmetic. On Twitter, heated discussions periodically flare up due to the results of calculating the following expression:

0.1 + 0.2 = 0.30000000000000004

But, as I already wrote , all popular programming languages ​​behave this way, even old-fashioned and pedantic ones like Haskell. Floating-point arithmetic may look strange, but they behave the same, their behavior is well-documented. Namely, we are talking about the IEEE 754 standard , the requirements of which are strictly implemented in programming languages. So, then the problem is not arithmetic: the implementation of addition, subtraction, division and multiplication in languages, programming can be said to be “carved in stone”.

Math object


My problem was in the standard JavaScript object Math. In particular, in all methods of this object.

Techniques such as Math.sin, Math.cos, Math.exp, Math.pow, Math.tan- is the basic ingredients for geometric and other calculations. When I understood this, I began to separately study the changes in the behavior of the basic methods of the object Mathin different versions of Node.js. Here are some examples.

Calculation Math.tanh(0.1):

// Node 4
0.09966799462495590234
// Node 6
0.09966799462495581907

Calculation Math.pow(1/3, 3):

// Node 10
0.03703703703703703498
// Node 12
0.03703703703703702804

Even worse, this problem is not only apparent in Node.js. The same thing happens in browsers, and in other environments that support JavaScript.

This leads us to the following question: what is mathematical calculation?


Graphical representation of calculations

Trigonometric methods are easy to visualize. If you have a single circle and several months of high school at your disposal, then you know that the cosine and sine are the coordinates of a certain point on the edge of the circle, and that the graphs of the functions sin and cos look like waves. In fact, in high school they are studying how to obtain these values, but the method used for this - the Taylor series - relies on an infinite series, and it is not easy for a computer to solve such problems.

Here's what you can learn from Wikipediaregarding the sine calculation algorithm: “There is no standard algorithm for calculating the sine. IEEE 754-2008, the most widely used standard for floating point calculations, does not affect the calculation of trigonometric functions like a sine. "

Computers use many different approximations and algorithms to perform calculations, something like CORDIC , all sorts of tricky tricks and lookup tables. All this heterogeneity explains the presence of many fastmath libraries on GitHub . The fact is that there are many ways to implement the method Math.sin. And other functions too. For example, as you know, Quake III Arena used a fasterreplacing the standard inverse square root calculation method to speed up rendering.

As a result, mathematical calculations are the result of the implementation of certain algorithms. In practice, many common algorithms and their variants are used.

The JavaScript specification, instead of specifying which particular algorithm to use in language implementations, gives implementations a lot of room for maneuver in terms of functions used in mathematical calculations.

Here is what the standard says about this (ECMA-262, 10 edition, section 20.2.2):

"The behavior of the functions acos, acosh, asin, asinh, atan, atanh, atan2, cbrt, cos, cosh, exp, expm1, hypot, log, log1p, log2, log10, pow, random, sin, sinh, sqrt, tan and tanh it is not fully described here, with the exception of the requirements for the return of certain results for specific values ​​of the arguments, which are noteworthy boundary cases.

I don’t know how the internal activities of the members of the committee responsible for the ECMA-262 standard work, but I believe that they made the standard so that there would not be a compatibility crisis in JavaScript if Intel or AMD issued new ultrafast mathematical instructions in their fresh processors.

Due to the fact that there are many widely used JavaScript interpreters, due to the fact that JavaScript is often used in browsers, and between browsers there is still something like competition, and due to the fact that even popular JavaScript implementations are under constant pressure and forced to evolve quickly, providing the best performance ... because of all this, we have what we have. Anyone who uses JavaScript will regularly experience the fact that in different implementations the results of mathematical calculations performed by the means of the object Mathare different.

This is not as significant in other interpreted languages, since they usually have some “canonical” implementations. For example, this is true for the Python interpreter.

Where are the calculations performed?


Now let's take a closer look at exactly where the calculations are performed. In the case of JavaScript, there are three areas in which basic mathematical calculations are performed:

  1. CPU.
  2. Language interpreter (C ++ and C code of a specific JavaScript implementation).
  3. JavaScript code, for example, code for specialized libraries.

â–Ť1. CPU


The first idea that came to my mind when I thought about the places where the calculations are made was that the calculations are performed in the processor. I suggested that since processors implement arithmetic calculations, they can also implement some more complex calculations. It turned out that the processors have instructions for performing trigonometric and other calculations, but these instructions are rarely used. For example, the implementation of the sine calculation in processors with x86 architecture was not very popular, since this implementation does not necessarily turn out to be faster than software implementations (those that use processor arithmetic operations). In addition, it is not necessarily more accurate than software implementations.

Intel also suffered a shame due to the very strongoverestimation of the accuracy of trigonometric operations in the documentation. Such errors are especially tragic due to the fact that a microchip, unlike a program, cannot be patched.

â–Ť2. Language interpreter


This is how the computing subsystems work in most JavaScript implementations. They implement these subsystems in various ways.

  • The V8 and SpiderMonkey engines use fdlibm library ports for calculations , which are slightly different. This library, originally written at Sun Microsystems, has been handed down from generation to generation.
  • JavaScriptCore (Safari) uses the cmath library to perform most operations.
  • Internet Explorer uses both cmath and some blocks of code written in assembler . Here even trigonometric methods of processors were used - in the event that the browser was compiled for processors that had similar instructions.

For historical reasons, the tools used to perform calculations in different JS engines have changed. So, V8 used its own solution for calculations, then the fdlibm JavaScript port was used, and only then the C version of fdlibm was used.

â–Ť Why is this a problem?


The point here is that all this reduces the ability of JavaScript to produce uniform results in solving any problems involving mathematical calculations. This is especially hard on Data Science. I would like JavaScript to be better suited for doing Data Science calculations in a browser. Moreover, the inability to produce uniform results means the aggravation of the reproducibility crisis , which is characteristic of all sciences. This is not to mention some other JavaScript problems, such as features of typing numbers and the lack of a widely used library for working with data frames.

â–Ť3. Using specialized libraries


There is a reliable way for us to do the calculations in JavaScript. It consists in using specialized libraries. So, the stdlib library implements high-level calculations using only arithmetic operations. Arithmetic calculations are fully described in the specifications, they are standard, so the results returned by stdlib give us, regardless of the platform on which the code is executed, completely uniform results.

This is achieved at the cost of complexity and speed of decisions. The stdlib methods are not as fast as the built-in methods. In addition, in order to "just count the sine", you need to connect a whole library to the project.

But, if you think more broadly, this is completely normal. The WebAssembly platform, for example, does not give the programmer any means to perform high-level mathematical calculations. In the documentation for it, it is recommended that you independently include implementations of the corresponding mechanisms in your own modules:

“WebAssembly does not include its own implementations of mathematical functions — like sin, cos, exp, pow, and so on. WebAssembly's strategy for such functions is to allow developers to implement them as library tools in the WebAssembly platform itself (note that the x86 platform's sin and cos instructions are slow and inaccurate, and these days, anyway, try not to use). ”

This is how compiled languages ​​always worked: when a program written in C is compiled, methods imported from math.hare included in the compiled program.

Using epsilon Value


If someone doesn’t want to include the stdlib library in his JavaScript project for performing calculations, but needs to test the code that performs some complex calculations, then he probably should resort to the method that is already used in the simple-statistics library. It is about using a value epsilonthat defines the boundaries within which the differences in numbers are not taken into account. If we consider the options for using the epsilon symbol in mathematics, then we can say that I am talking about it as an “arbitrary small positive value”. Simple-statistics uses the epsilon value of equal 0.0001.

If you need to find out if two numbers are equal, a condition of the form is checkedMath.abs(result — expected) < epsilon. If this condition turns out to be true, then we can say that the difference between the numbers falls within the given range and consider them equal.

Additions


â–Ť Accuracy


Commentators on Twitter pointed out that the variations in the results obtained in the example are outside the range of significant digits of the floating point number. From a technical point of view, this is correct, and this means that you can find a more accurate way to compare numbers than the one that involves using a value epsilon. But in practice the same story here - the numbers at the end of the number affect the result and introduce inaccuracies in the final result. In addition, the examples given here are not exhaustive. The fact is that the implementation features of JavaScript interpreters can, without departing from the specification, lead to the appearance of differences in most of the numerical results.

â–ŤJavaScript


I do not want to criticize JavaScript. I believe that JavaScript made a justifiable compromise, taking into account the uncertainty of the future and the number of platforms on which language implementations are created. I must say, it is very difficult to directly compare JavaScript and other languages. The point is the JavaScript ecosystem. The fact that at the same time there are many interpreters of the same language is completely atypical for other languages. And this, in addition, is one of the main strengths of JavaScript. Further, one cannot fail to say that these are phenomena of a completely different plan than those that occur in the language itself. And JavaScript changes over time and a lot of good things appear in it .

â–ŤStdlib or epsilon?


I believe that in practice in most cases it is worth using the approach that implies the use of the epsilon value. The stdlib library is a wonderful powerful tool, but the price of including an additional library for mathematical calculations in the project can be quite high. And in most cases, small discrepancies in the results of calculations do not matter for applications.

Summary


Here I would like to draw conclusions from the foregoing and share some thoughts.

  1. , , , . . — « ». , , , Math.sin , , , . , , , , , . , , , .
  2. . , , Node.js, , , simply-statistics. , , , , . — .
  3. , . V8, , . , . , , , .

Dear readers! Have you encountered problems regarding changes in calculation results when upgrading to new versions of Node.js?


All Articles