The built-in function Math.log1p(x)
calculates the value of the expression
Math.log(1 + x). At first glance it may seem
strange to include such a simple helper, but its purpose is not convenience but to preserve
precision when x is very small. Take a look at the following chart:
Note how once we reach input values smaller than around 10-13 we start losing
significant amounts of precision while Math.log1p does not. To understand why we have to look at
how floating point works. Floating point values are stored in 3 components - a sign bit, a
significand, and an exponent. The value of the floating point is determined by evaluating
known as double precision floats) and allocate 1 bit for the sign, 11 bits for the exponent,
leaving 52 bits for the significand.
So now let's look at our calculation of Math.log(1 + x) when x is very small. Since we have a
very tiny number less than 1, wecan express all the leading zeroes as a negative exponent and
reserve all of the significand to express precision following the first non-zero digit. But when
we add 1 to this number, we end up with a number very slightly more than 1. In this case, we
need to set the exponent to 0, forcing us to use the significand to encode all of the subsequent
0's after the initial 1. This leaves us significantly fewer bits available for precision since
most of the significand is now dedicated to encoding the 0's following the initial 1.
To illustrate this problem further, you can evaluate
0 + 1E-100 === 0 as compared to
1 + 1E-100 === 1 and note that the former is true while the latter is not. This is
more than 1, but slightly more than 0 is no problem because we can use a negative exponent.