Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
9999999999999999.0 – 9999999999999998.0 (sdf1.org)
98 points by tosh on Jan 11, 2024 | hide | past | favorite | 80 comments


> That Go uses arbitrary-precision for constant expressions seems dangerous to me.

I'm somewhat baffled by this statement. If a Go program compared a constant expression float against a runtime computed float, it could have unexpected results, but comparing floats in general is dangerous. I don't see how this language quirk increases that danger in a meaningful way.


It's not an issue an issue about whether or not efficient float formats are dangerous to work with rather one of whether they are consistent to work with in a language. It's simple enough to remember when reading about it but will you and everyone else know this from the start and remember it every time it comes up without fail?

That said, for the negatives it comes with it does come with positives as well and I think that makes it worth it.


It's one thing if a language outputs one, two, or zero for this expression. Two and zero are mathematically wrong, but at least they're predictable.

It's another thing if a language sometimes outputs two and sometimes outputs one depending on the syntax of the request. This is reasonable where that syntax change is not an explicit cast to double precision or single precision, but dangerous if it uses a behind-the-scenes default of arbitrary precision in compiled literals and a default of FP32 in implicitly typed literal assignments.


One unexpected consequence is the behavior around negative zero. For example:

    var d = -0.0
is different than

    var d = 0.0
    d = -d

This IMO crosses the line into "outright bug" territory.

https://play.golang.com/p/X-JR9NdiCIC


IEEE754 is always fun. 9007199254740993 is the first integer that cannot be represented as a double precision float. So in python:

    >>> 9007199254740993.0
    9007199254740992.0
What's really surprising is that this number is only ~16 million in single precision floats.


> What's really surprising is that this number is only ~16 million in single precision floats

Half of all floats are in [-1, 1].


Any real number type must have this property is you expect 1/x to work correctly.


Floating point is all about relative precision. f64 gives you 53 bits of mantissa, so you have precision of about 1.1e-16, almost 16 decimal places. But the 17th place, and sometimes the 16th place, gets clobbered.

Integers have absolute precision, at the expense of either range (say, i64) or arbitrarily growing size.


> What's really surprising is that this number is only ~16 million in single precision floats.

What does this mean? Surely even single precision floating point can represent a number far closer to the original than 16 million? Edit: It appears the closest number in single precision is 9007199000000000.0

Edit2: Oh I see: you mean that the first number that cannot be represented precisely by a single-precision float is ~16 million.


bc -l is my standard command-line tool for arbitrary precision calculations (-l not needed here but handy for functions like sqrt(), e(), and log l() ):

    $ bc -l
    bc 1.06
    Copyright 1991-1994, 1997, 1998, 2000 Free Software 
    Foundation, Inc.
    This is free software with ABSOLUTELY NO WARRANTY.
    For details type `warranty'. 
    9999999999999999.0 - 9999999999999998.0
    1.0


I got tired of typing bc -l years ago, I have a function so I can do it on the command line: calc 2^27 + 3^15. I wish somehow the shell would let me add parens here, instead of having to escape them with a quote.

function calc2 { if [ $# -eq 0 ]; then echo 'pass commands for math evaluation, like calc l(2^32/17) + 3'; fi; echo $* | bc -l; }


This is what I've had in my bashrc for a few years:

= () { printf "%'.f\n" $(echo $1 | bc); }; alias calc==


bc is my calculator for pretty much everything for decades now. lol


who does not use bc? people who aren't old unix people ;-) I use bc


I consider myself an old unix person, but I use dc. Possibly to make it more confusing to anyone who looks at what I'm doing.

It also gives the math result here of course.

  % dc
  9999999999999999.0 9999999999999998.0 - p
  1.0
  %


I guess old unix persons do use bc. But even older unix persons, like you and me, use dc. Oh, and I still have my trusty old HP48GX calculator that I bought nearly thirty years ago. Algebraic notation is fine for paper, bur RPN is best for calculating. I am glad I can even have an RPN calculator (PCalc) on my phone now.


rpn for for the win I guess ;-). I actually didn't get to try my first linux stuff until grad school in the 80s. I wish I had saved off all my init files from then to see what they look like now.


Back in the day (Unix Seventh Edition) bc was just a front end that compiled the expression and piped it to dc. My take it is most people didn't find rpn a win, and bc was the fix.

I used bc's compiled output ("bc -c") to learn how to make dc jump through hoops.

https://man.cat-v.org/unix_7th/1/bc


Floating-point literals are confusing. The basic arithmetic less so (although it too has some pitfalls). I said this before, but I think inexact literals (which are very common) should cause warnings in languages where used.

    >>> 9999999999999999.0 == 10000000000000000.0
    True
    >>> from decimal import Decimal as D
    >>> D(9999999999999999.0)
    Decimal('10000000000000000')
"It's a simple question" I find it very much not simple question; indeed I'm not sure what is the expected "right answer" here?


Emacs Calculator with high precision will get you what you need (`M-x calc RET P 20 RET 9999999999999999.0 RET 9999999999999998.0 RET -` gives you `1.`)

Definitely keeping this example in my back pocket for explaining to people why floating point is not what you think it is.


That reminds me, emacs was truly the best Operating System.


Floating point 101. And it almost never matters.


> And it almost never matters.

I take issue with this. Drift from floating point inaccuracies can compound quickly and dramatically affect results. Sure if you're just looping over a 1000 item list, it's not going to matter that JavaScript is representing that as a float/double, but in a wide variety of contexts, such as anything to do with money, it absolutely does matter.


Another example is open world games. They have to keep world coordinates centered on the player because for large worlds, the floating point inaccuracy in the far reaches of the world starts to really matter. An example of a game that does this is Outer Wilds.


caveat: this is not my direct experience so i might be wrong -- but someone who was doing an different masters project at the same time as mine was doing a mini on-rails video game and mentioned this.

apparently it's also because of the "what is up?" question.

e.g. in outer wilds ... how do you determine which way is "up" when "up" for the player can be any direction.


Oh for 9999999999999999 pennies! I wouldn't care if I got one less that I was supposed to! :-)


You say that, and then an army of idiots out in the real world continues to use floats for financial data and other large integers.

I ran into a site that broke because they were using 64b unix nanotime in Javascript and comparing values which were truncated. You see this in js, python, etc. constantly.


For the JS case, that's really JavaScript's fault, since double-precision float ("number") is the only built in numeric type, other that BigInt, which has only existed for a few years.


Not just floating point, but 64-bit IEEE 754 specifically. The last few bits of the mantissa are not sufficient to represent the last decimal digit exactly. 80 bits would suffice for this particular example, but would fail similarly with a longer mantissa.

BTW this is one of the reasons why you should never represent money as a float, except when making a rough estimate. Another, bigger reason is that 0.1 is an infinite repeating fraction in binary, so it can't be represented exactly.


For me, I encounter a floating point bug/issue every 4 years of so. So "almost never" sounds about right to me.


I encounter bugs around this semi-rarely, but most of my career has been building tools around data analytics. While it's rare that I encounter bugs tied to floating point it's frequent that I need to be aware of floating point math and if a float will be acceptable here. The rareness of the bug has more to do with it being a rookie mistake that won't make it past code review than "it doesn't matter" as the comment implies.


Moral of the story: we all should switch to programming in Perl6.


Which is called the Raku Programming Language (#rakulang) since 2019.

    $ raku -e 'say 9999999999999999.0 - 9999999999999998.0'
    1
https://raku.org


First note that it was renamed to Raku in 2019.


Here's a great introduction to floating-point numbers: https://tobydriscoll.net/fnc-julia/intro/floating-point.html

The next representable number after 9999999999999998.0 is 1.0e16. 9999999999999999.0 is not exactly representable in IEEE 754 floats, and will be rounded up or down.

The difference between 0.0 and 2.0 in the table is likely due to different rounding modes. I'm curious how different languages end up with different rounding modes. Is that possible to configure?

    julia> 9999999999999999.0 - 9999999999999998.0
    2.0

    julia> -9.999999999999999 + 9.999999999999998
    0.0


It's not different rounding modes, but different floating point formats. Google is doing something very weird that is also base 10 related. Not sure about TCL.


TCL does for quite some years now use real integers (unbounded size) and floats (float8) whenever possible. By the way, the article does not specify versions of languages, so:

    $ tclsh
    % puts $tcl_version
    8.6
    % expr "9999999999999999.0-9999999999999998.0"
    2.0


In Ada:

    with Ada.Text_IO; use Ada.Text_IO;
    procedure Example is
    begin
        Put_Line (Float (9999999999999999.0-9999999999999998.0)'Image);
    end Example;
Result:

     1.00000E+00
If I really wanted to, I could use a decimal instead, e.g.

    with Ada.Text_IO; use Ada.Text_IO;
    procedure Example is
        type Decimal is delta 1.0E-20 digits 38;
    begin
        Put_Line (Decimal (9999999999999999.0-9999999999999998.0)'Image);
    end Example;
Result:

     1.00000000000000000000


Postgres and SQL:

  postgres=# select 9999999999999999.0 - 9999999999999998.0 as result;
   result
  --------
      1.0


The default PG type, `numeric`, has almost arbitary precision.

    select pg_typeof(9999999999999999.0); 
    -- numeric

    select 9999999999999999.0::double precision 
           - 9999999999999998.0::double precision; 
    -- 2
More interesting perhaps, is mixing up `real` (aka float32) with `numeric`:

    select 9999999999999999.0::real - 9999999999999998.0;
    -- 272564226 (?! can anyone explain?)

    select 9999999999999999.0::real;
    -- 10000000300000000
Wat


The `real` type (float32) only has 24 bits of precision. So converting `9999999999999999.0` or `10000000000000000` or even `10000000300000000` yields the 32-bit float `10000000272564224`.

For some reason Postgres prints it as `10000000300000000`. It uses a heuristic to print a "pretty" number that converts to the actual stored value, and it's not smart enough to give `10000000000000000`. Some heuristic like this is needed so something like `0.3` doesn't print the actual stored value of `0.300000011920928955078125`, which would be confusing.

You can check all this here: https://www.h-schmidt.net/FloatConverter/IEEE754.html


    #include<stdio.h>

    int main(void) {
      long a = (float)9999999999999999;
      long b = 9999999999999998;
      printf("%ld - %ld = %ld\n", a, b, a-b);
    }

produces

    10000000272564224 - 9999999999999998 = 272564226


>>> from decimal import Decimal

>>> Decimal("9999999999999999.0") - Decimal("9999999999999998.0")

Decimal('1.0')


For Perl, you just need the core module 'bignum'.

perl -Mbignum -E 'say 9999999999999999.0-9999999999999998.0'


Those of us who back in the day saw Windows 3.11 calculator return something like 2.11-2.1=0 are not impressed.


The page never explicitly states what the right answer is, but based on the output of their suggested "correct" perl, we can infer that they expect 1.

This is just me being idiosyncratic I guess, but if I see a number with a decimal place, I default to interpreting it as an fp64 unless otherwise specified - which yields a "correct" answer of 2.0 (which is not an answer I can get to in my head, admittedly)

If the question-asker wanted integer arithmetic, they'd have left off the ".0", and if they wanted something other than fp64 roundTiesToEven they should've been more explicit :P


The page is asking which language answers the math question correctly, not which one implements a particular standard correctly. Perl6 also has a correct implementation, it's just using a different standard than the other languages.


"math" is just yet another (implicit and at times poorly defined) standard.

The answers are all equally "correct" given a particular set of operating rules, the interesting part is what rules they picked.

Edit: See also, https://en.wikipedia.org/wiki/Definitions_of_mathematics - "Mathematics has no generally accepted definition"


I agree that what we call "math" is just a set of rules that could be defined differently, but a standard like IEEE-754 is downstream of math, and is explicitly defined as a method to perform arithmetic (which is part of math).

So they aren't on the same level, IEEE-754 is not an alternative standard to math. The answers can all be considered correct by a certain definition, but they are not equally correct in our shared context as human beings who know what math is.


Math is not a singular set of rules, it depends on who you ask, and what the context is. We have a pretty decent shared understanding, but it's not perfectly uniform.

59 + 1 can very reasonably be 1:00, for example.

I'm not saying 2.0 is the correct answer, I'm just saying it's not any less correct than 1 is.


I wasn't surprised, but that is not what I would expect if I were to see that expression. Two reasons for that: even though there is a decimal, I still mentally parsed it as an integer, and I wouldn't be thnking of the precision of the value.

(That said, I would never enter an integer with a decimal, and I would think of precision if it was a float. That said that said, there are other ways to bump into that problem which wouldn't make it so obvious - such as dealing with inputs from a user.)


The pointing finger is not the moon... 1 is the mathematically correct answer, and the floating point behavior is a noisy approximation.


Could you please explain me why 2 would be the correct answer?


Parsing the provided numbers to two IEEE 754 double-precision floats, and then subtracting them, yields 2.0 (due to rounding)

In particular, 9999999999999999.0 rounds up to 10000000000000000.0

(note, this behavior depends on the rounding mode, https://en.wikipedia.org/wiki/IEEE_754#Rounding_rules )


But here rounding is giving you the wrong answer.


I was curious about `soup`, clicking on it, the author states:

>soup is a programming language I've been working on for a few years that is designed to make it easier for the programmer to write fast, robust programs with no bugs. >Availability >I'm sorry to say soup is not yet available for general use.

Now I am curious what this is.


Was curious what chatGPT would say:

The result of subtracting 9999999999999998.0 from 9999999999999999.0 is 2.0. This result is due to the limitations in the precision of floating-point arithmetic in computers. When dealing with very large numbers, the precision can be lost, leading to such unexpected results.

I suppose this must be in its training set.


these days it looks like ChatGPT generates and runs code behind the scenes for math questions (it used to be pretty bad at these kinds of math questions. you can click the little "view analysis" button to see the code it's running.

when I ask about this question it generates and runs Python behind the scenes, thus the incorrect answer.


Although it offers an explanation, which is something it could not do by just evaluating the expression. So the explanation is in its training set. And given that this is an understood flaw or limitation, it is possible it didn't even need to run any code.


Hmm, I asked Bing Chat in Precise mode and got the correct answer:

Q: What is the result of subtracting 9999999999999998.0 from 9999999999999999.0

A: The result of subtracting 9999999999999998.0 from 9999999999999999.0 is 1.0. Here is the calculation: 9999999999999999.0−9999999999999998.0=1.0

I also tried a variation and it's still correct, maybe it's using a calculator plugin:

Q: What is the result of 1239995999999999.0 - 1239995999999998.0

A: The result of the subtraction operation 1239995999999999.0−1239995999999998.0 is 1.0


Just asking for the subtraction it just gives me 1.0, what did you ask?


Nim gives the wrong answer (2.0). I've posted this to the Nim forum: https://forum.nim-lang.org/t/10866#72441


To be clear, Nim is going to be beholden to the platform C implementation it is compiled on for its floating point behavior.


I thought that may be the case.



The GoLang bit tells me that the author is trying to show wrong answers only. And that is ok because this needs to be said out loud.

I wonder if there is a way to fool Wolfram in giving the wrong answer. Or ChatGPT?


ChatGPT 4 pondered for a while, then started "Analyzing..", and eventually "Finished analyzing" (all following verbatim output):

python 9999999999999999.0 - 9999999999999998.0 Result 2.0

The result of the calculation 9999999999999999.0−9999999999999998.0 is 2.0.

This result may seem counterintuitive, but it's due to how floating-point numbers are represented and handled in computing. Floating-point arithmetic can introduce small rounding errors, especially with very large numbers, as precision is limited. In this case, the precision required to represent these large numbers exactly is beyond what typical floating-point representations (like IEEE 754 double precision) can handle, leading to a slight error in the calculation.


> result may seem counterintuitive

does it say this out loud?


Wolfram seems to excel at so many things, but does anyone actually use it?

https://www.wolframalpha.com


Only every high school and college student everywhere.


No kidding. I don't trust my own by hand calculations anymore. And why bother? I'd rather just write down the equation I need and let a computer do the rest.


Almost every day for my 5+ years of college, and fairly regularly since (although I encounter the types of complex math problems it excels at far less frequently today). I think there are many others like myself who use it.


any time you want to do a bunch of unit conversions. I was doing some napkin math comparing Tadej Pogačar on a 1200 W e-bike to him on a gas dirtbike (gas dirtbike smokes e-bikes, btw) and it really helped with all the weights and powers being specified in different units between the two. Not just one conversion but when there are a bunch of steps that can introduce errors.


x86 has 80-bit precision FPU and 80-bit regs, so the result can be different depending on whether it is a reg-reg or a mem-mem (float64) operation.


Chicken Scheme gets it wrong ``` #;1> (- 9999999999999999.0 9999999999999998.0) 2.0 ```


So does GNU Guile and Gauche Scheme


I will have to try it with GNU COBOL. I know on the mainframe this will work 100%.


Wow, sdf is still around! I wonder how they're doing.


php > echo 9999999999999999.0 - 9999999999999998.0; 2


haskell with default prelude is 2.0. Elm is 2 : Float.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: