Okay, so it starts with my delayed math education. As part of my Computer Science program, I had access to world-class math professors, access that I mostly wasted. I did not like math: the topics were so removed from practice, and I was already frustrated by the highly theoretical, and – I thought at the time and mostly still do – out-of-touch CS program.
Unfortunately, a few years after graduating, I got the hunger for math. Seeing how I could apply just a little bit of math knowledge to great effect in my work & hobbies had me inspired. But I had no clear way of learning it.
So what I noticed over the years was that tests kept breaking when I updated Node. I’d have a test like:
What it was, was Math. In particular, all of the methods that come after Math.
Methods like Math.sin, Math.cos, Math.exp, Math.pow, Math.tan: essential ingredients for geometry and basic computation. I started isolating changes in basic function behavior between versions. For example:
Calculating Math.tanh (0.1)
// Node 4 0. // Node 6 0. Calculating Math.pow (1/3, 3)
1: The CPU
This was my first guess: I assumed that since CPUs implement arithmetic, they might implement some higher-level math. It turns out that CPUs do have instructions to do trigonometry and other operations, but they’re rarely invoked. The CPU (x ) implementation of sine doesn't get much love because it's not reliably faster than an implementation in software (using arithmetic operations on the CPU), nor as accurate.
Intel also bears some blame for
overstating the accuracy of their trigonometric operationsby many magnitudes. That kind of mistake is especially tragic because, unlike software, you can't patch chips.
(2: The language interpreterWhy this is an issue
This is how most of the implementations do it, and they implement math in a variety of ways.
replication crisis in the sciences.
The third way
This comes at the cost of complexity and speed: stdlib isn’t consistently as fast as built-in methods, and you’ll need to require a library ‘just’ to compute sine.
But in the wider view, this is pretty normal! WebAssembly, for example, does not give you higher-level math methods at all and recommends you include a math implementation in your modules themselves:
“WebAssembly does not include its own math functions like sin , cos, exp, pow, and so on. WebAssembly’s strategy for such functions is to allow them to be implemented as library routines in WebAssembly itself (note that x 125 ‘s sin and cos instructions are slow and imprecise and are generally avoided these days anyway). ”
And this is the way that compiled languages have always worked: when you compile a C program, the methods you import from math.h are included in the compiled binary.
(Using an epsilon)
If you don’t want to include stdlib to do math but you do want to test math-heavy code, you’ll probably have to do what simple-statistics does right now: use an epsilon. Of the 5 uses of epsilon in math , the one I’m referring to is “an arbitrarily small positive quantity” . It’s a tiny number. Here’s simple-statistics’s implementation : the number 0.0 .
You then compare
Math.abs (result - expected)
Here’s where I was a little short on time in person and have some room to expand.
First, what’s under the hood is rarely what you expect. Our Current tech stack is heavily optimized and a lot of optimizations are really just dirty tricks. For example, the number of hardware instructions it takes to solve
Math.sin varies based on the input, because there are lots of special cases. When you get to more complex cases, like ‘sorting an array’, there are often multiple algorithms that the interpreter chooses between in order to give you your final result. Basically, the cost of anything you do in an interpreted language is variable.
Second, don't trust the system too much. What I was seeing between Node versions really should have been a bug in the testing library, or something in my code, or maybe in simple-statistics itself. But in this case, digging deeper revealed that what I was seeing was exactly what you don't expect: a glitch in the language itself.
Third, everyone's winging it. Reading through the V8 implementation gives you a deep appreciation of the genius involved in implementing interpreters, but also an appreciation that it's just humans doing the implementation: they make mistakes, and, as evidenced by the constantly-changing algorithms for mathematics, always have room to improve.