golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.05k stars 17.54k forks source link

math: Nextafter, Nextafter32 clarify behavior with -0, +0 #42613

Open nsajko opened 3 years ago

nsajko commented 3 years ago

What version of Go are you using (go version)?

$ go version
go version go1.15.4 linux/amd64

Does this issue reproduce with the latest release?

Yes.

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/tmp/freedesktopCache/go-build"
GOENV="/home/nsajko/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/nsajko/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/nsajko"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/lib/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/lib/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build930141060=/tmp/go-build -gno-record-gcc-switches"

What did you do?

Some explanations are in the comments: https://play.golang.org/p/vaQBl7C6FNm

What did you expect to see?

Nextafter(x, y) should return the next representable
float64 value after x towards y, because none of the
three special cases apply here, because negative and
positive zeros and 1 and -1 are neither the same value
nor NaN.

-4.941e-324 -0 0 0
-0 -0 0 4.941e-324

What did you see instead?

...

-4.941e-324 -0 -0 4.941e-324
-4.941e-324 0 0 4.941e-324

Other notes

Relevant observation: negative and positive floating point zeros are distinct values in Go, but compare equal with the == operator. This is OK, but could be considered as the cause of the bug in the implementation.

Inconsistency with other finite float64 arguments

The inconsistency with the handling of zero arguments causes unexpected behavior when looping using the Nextafter functions.

This is not an actual issue for me, but it seems like it might be for someone who uses Nextafter: assuming a nonzero negative number x is chosen, start iterating on it like this: x = math.Nextafter32(x, 0), with the aim of breaking the loop at zero (a similar situation is observed when starting from a positive value, instead of a negative one). The loop, unexpectedly, will never exit in some cases: https://play.golang.org/p/eWs0pg8SXri

Inconsistency with C

Even though at first I thought that this is a relatively straightforward issue (because of the "next representable float64 value" explicit wording), this actually seems to be a tricky issue if compared with C, and it's even possible you may want to consider this a documentation bug instead of a behavioral bug, along with a couple other options.

The C nextafter functions were supposedly the inspiration for the Go version so it might be relevant to compare to them, also, it might be desirable to be compatible with them if you're appropriating their names already.

Both C17 and the current C draft have this to say about nextafter:

Description
The nextafter functions determine the next representable value, in the type of the function, after x
in the direction of y, where x and y are first converted to the type of the function.244) The nextafter
functions return y if x equals y. A range error may occur if the magnitude of x is the largest finite
value representable in the type and the result is infinite or not representable in the type.

Returns
The nextafter functions return the next representable value in the specified format after x in the
direction of y.

Notice the return y if x equals y explicit requirement about zeros - this is where C differs from Go currently.

But, except for that, the C implementations are actually like Go: they don't follow the spec when x is negative zero and y is a positive nonzero value, or when x is positive zero and y is a negative nonzero value. (In that case the other zero is "skipped over".)

C program for comparison (give it "0" on its stdin):

#include <math.h>
#include <stdio.h>

int
main(void) {
        int n;
        scanf("%d", &n);
        double x = n;
        double negX = -x;
        printf("%.4g %.4g %.4g %.4g\n%.4g %.4g %.4g %.4g\n",
            nextafter(negX, (double)-1), negX, nextafter(negX, x), nextafter(negX, (double)1),
            nextafter(x, (double)-1), nextafter(x, negX), x, nextafter(x, (double)1));

        return 0;
}

Outputs:

-4.941e-324 -0 0 4.941e-324
-4.941e-324 -0 0 4.941e-324
randall77 commented 3 years ago

This all depends on the phrase

the next representable float64 value

I would interpret the next representable value after -0 to be 4.941e-324, not 0. 0 is not "after" -0 in my mind, as 0 == -0. I can certainly see how that might be confusing, though.

We could add additional special cases to the docs, if you think that would help. Or was there something else you had in mind?

nsajko commented 3 years ago

I see these options:

  1. document the special cases around zero and keep the behavior the same
  2. have a special case for x == y (return y), like in C standard and implementations, otherwise keep behavior the same. I don't know where does this matter, but it might make sense to align with the C precedent.
  3. switch to the way I, at least, interpreted the "next representable float64 value" phrase. I.e., 0 and -0 are not the same value.
randall77 commented 3 years ago

I vote for 1. We don't want to violate the Go 1 compatibility guarantee. I guess you could argue that this fits under the "bugs" exception, but it is not clear to me that it is even a bug. I think we'd need a stronger argument to make a backwards-incompatible change.

cagedmantis commented 3 years ago

/cc @griesemer @rsc