1. 12
  1. 8

    I’d have a test like: t.equal(ss.gamma(11.54), 13098426.039156161);

    record scratch

    OK, that’s your problem right there. It’s well known that you don’t write tests like this: instead you always leave some margin for error in FP equality assertions.

    There’s difference between precision and accuracy. Just because IEEE doubles give you 52(?) bits of mantissa doesn’t mean those bits are all correct down to the LSB. And in cross platform code, like anything JS, you can’t even trust those bits will be 100% consistent from one platform/version to the next.

    If you make assertions like that test, you’re basically asserting that the implementation of every math function you call remains exactly the same, which is a bad idea. You’re testing stuff that’s not part of the contract of the API, basically.

    1. 12

      Did you read the rest of the post? It explains this, just less condescendingly. You write that “It’s well known” not to write tests like this, but where exactly are people supposed to learn that? Posts like this seem like as good a place as any.

      1. 4

        Yeah, for sure. It nearly always comes off as gatekeepey and obnoxious to write “It’s well known that …” (comparable to the “It’s obvious that …” in a lot of math textbooks), and even when it doesn’t, it is never to the benefit of the reader. So cut it out if you do it!

        I learned something from this blog post because I’ve run into this exact behavior when writing unit tests. I used the epsilon approach to dealing with it mostly because that was the only way that made sense to me – glad to know that is a reasonable approach, and good to know about the others

        1. 1

          Well, I was told about floating point arithmetic’s gotchas at least on 8 different courses on the university. Is learning what you are doing gatekeeping? Such is life. ¯_(ツ)_/¯

          1. 4

            No, learning is not gatekeeping. It’s the tone one uses to express that learned knowledge what can be perceived as gatekeeping. Some people are great at enthusiastically sharing their knowledge with others, which is completely different than simply stating “it is well known”, which sounds very much like “you should have already known this”.

            About this topic in particular, i think it is totally reasonable to conclude that floating point operations are stable and well defined given the admittedly limited experience of noting that Math.sin(x) has always returned the same value when called with the same argument. It’s learning by doing, and that’s how many many people learn programming. Only when the result changes, because of a different browser or Node version, this assumption would be challenged, which i think is what the blog post is trying to convey :)

        2. 3

          I did read it; see my reply to @hwayne. I think the author misunderstands the nature of FP math: complex operations are always approximations, and roundoff errors always occur. (Trig functions are mostly computed as Taylor series, and there’s always a decision of how many terms to add before stopping.)

          Sorry for sounding snarky. This is a bit of a pet peeve of mine, but I shouldn’t have climbed on my high horse so readily.

          1. 1

            where exactly are people supposed to learn that?

            In just about any course on floating-point arithmetic. This error is not even a quirk of a hardware implementation. It shows that this person has never seen how errors propagate through FP operations, how FP works in the abstract (with all those (1 + ε) factors). It’s fundamental knowledge. Similarly, I wouldn’t expect an article about strcpy(3) causing buffer overflow in some situations or the discovery of two zeros in FP to get this much attention.

            Posts like this seem like as good a place as any.

            Learning programming from blog posts is a terrible idea, akin to learning programming from Stack Overflow answers. That’s how you learn trivia, not systematic knowledge. It’s also how you assimilate a lot of misinformation. Well-reviewed books (or lectures) are the way.

          2. 5

            He explicitly discusses epsilons in the article.

            1. 4

              Yes, but only as a workaround for when you can’t afford to lug a custom JS reimplementation of the entire math library around with you. IMHO that’s the wrong way to look at it. Asking for accuracy down to the last bit in complex FP operations is not realistic. (For example, what happens if someone in the future finds a bug in that JS math lib, or an optimization, but can’t deploy it because it’ll break every use of it that expects perfect precision?)

              1. 3

                This is why the generally accepted modus operandi is to use binary coded decimal arithmetic for financial calculations, where “exact” solutions are needed (and usually only addition/subrtraction/mutiplication and in extreme cases division are used), and floating point arithmetic is used for engineering (as in real engineering, not ad-tech and div layout engineering) , where working with tolerances has been the norm since centuries.

          3. 4

            This article seems to be based on a flawed understanding of floating point numbers. The author complains that “sin(x) gives me different results in different versions of Node”. What the author does not demonstrate in the article is the understanding that floating point numbers are approximations themselves, i.e. a given pattern of bits does not represent a precise number, but rather some range of numbers. Consequently, sin(x) is not well defined because x itself is not well defined.

            1. 2

              a given pattern of bits does not represent a precise number, but rather some range of numbers.

              Do you have a source for this? I don’t think this is correct - my understanding is that while every floating point number has an infinite number of numbers that will round to the same value, a given pattern of bits has a single canonical value that it represents.

              As the author points out, while floating point operations like addition/etc can exhibit surprising results, they are well defined, in that any two floating point numbers have a specific floating point number that is their sum. The fact that addition/subtraction/etc have this property, but sin/etc do not is somewhat interesting.

              1. 1

                It’s not so much that the numbers themselves are approximations; it’s that the formulas used to compute trig functions are infinite series. The more terms you add up, the more accurate the result, but the longer it takes. I’m not an expert on current FP code, but I doubt it keeps grinding out terms until the result stops changing. Instead there’s a balance between speed and accuracy. And of course different implementations have different trade-offs.

                1. 1

                  There used to be LUT accelerated solution in i387 and i486+ CPUs. Since SSE2 there are fast trigonometric functions (faster and simpler to call I think) than the i387 floating point solutions. Switching between these implementations may result in different results. Software solutions are available, based on the Taylor series of the functions, but those are multiple magnitudes slower, and I doubt that node would rely on those.

                  Also floating point numbers do have a canonical value per se, but floating point arithmetic is an approximation, and it is expected to be treated as such.

                  1. 2

                    For sine in particular, because a transcendental constant is involved, we have what Kahan called The Table-Maker’s Dilemma: How do we round the result?

                2. 1

                  Even the sum may change in a way “unexpected” by the author:

                  Even the conversion between the binary and decimal representation of a floating point binary number is (as in printf/scanf) an approximation, and this may change with node version (if they ship their solution and don’t use libc). Theoretically even a microcode update may effect floating point operations (although not likely).

                  Thus even by having the same Math.js deployed everywhere there are a multitude of variables at play on lower levels. Here cross-platform differences have been generously ignored.