Always false float comparisons
Timon Gehr via Digitalmars-d
digitalmars-d at puremagic.com
Wed May 18 10:10:25 PDT 2016
On 17.05.2016 23:07, Walter Bright wrote:
> On 5/17/2016 11:08 AM, Timon Gehr wrote:
>> Right. Hence, the 80-bit CTFE results have to be converted to the final
>> precision at some point in order to commence the runtime computation.
>> This means
>> that additional rounding happens, which was not present in the
>> original program.
>> The additional total roundoff error this introduces can exceed the
>> roundoff
>> error you would have suffered by using the lower precision in the
>> first place,
>> sometimes completely defeating precision-enhancing improvements to an
>> algorithm.
>
> I'd like to see an example of double rounding "completely defeating" an
> algorithm,
I have given this example, and I have explained it.
However, let me provide one of the examples I have given before, in a
more concrete fashion. Unfortunately, there is no way to use "standard"
D to illustrate the problem, as there is no way to write an
implementation that is guaranteed not to be broken, so let us assume
hypothetically for now that we are using D', where all computations are
performed at the specified precision.
I'm copying the code from:
https://en.wikipedia.org/wiki/Kahan_summation_algorithm
$ cat kahanDemo.d
module kahanDemo;
double sum(double[] arr){
double s=0.0;
foreach(x;arr) s+=x;
return s;
}
double kahan(double[] arr){
double sum = 0.0;
double c = 0.0;
foreach(x;arr){
double y=x-c;
double t=sum+y;
c = (t-sum)-y;
sum=t;
}
return sum;
}
double kahanBroken(double[] arr){
double sum = 0;
double c= 0.0;
foreach(x;arr){
real y=x-c;
real t=sum+y;
c = (t-sum)-y;
sum=t;
}
return sum;
}
void main(){
double[] data=[1e16,1,-9e15];
import std.stdio;
writefln("%f",sum(data)); // baseline
writefln("%f",kahan(data)); // kahan
writefln("%f",kahanBroken(data)); // broken kahan
}
In D, the compiler is in principle allowed to transform the non-broken
version to the broken version. (And maybe, it will soon be allowed to
transform the baseline version to the Kahan version. Who knows.)
Now let's see what DMD does:
$ dmd --version
DMD64 D Compiler v2.071.0
Copyright (c) 1999-2015 by Digital Mars written by Walter Bright
$ dmd -m64 -run kahanDemo.d
1000000000000000.000000
1000000000000001.000000
1000000000000000.000000
Nice, this is what I expect.
$ dmd -m64 -O -run kahanDemo.d
1000000000000000.000000
1000000000000001.000000
1000000000000000.000000
Still great.
$ dmd -m32 -run kahanDemo.d
1000000000000000.000000
1000000000000001.000000
1000000000000000.000000
Liking this.
$ dmd -m32 -O -run kahanDemo.d
1000000000000000.000000
1000000000000000.000000
1000000000000000.000000
Screw you, DMD!
And suddenly, I need to compile and test my code with all combinations
of compiler flags, and even then I am not sure the compiler is not
intentionally screwing me over. How is this remotely acceptable?
> and why an unusual case of producing a slightly worse
It's not just slightly worse, it can cut the number of useful bits in
half or more! It is not unusual, I have actually run into those problems
in the past, and it can break an algorithm that is in Phobos today!
> answer trumps the usual case of producing better answers.
> ...
The 'usual case of producing better answers' /is not actually
desirable/, because the compiler does not guarantee that it happens all
the time! I don't want my code to rely on something to happen that might
not always happen. I want to be sure that my code is correct. I cannot
conveniently do so if you don't tell me in advance what it does, and/or
if the behaviour has a lot of abstraction-breaking special cases.
>
>> There are other reasons why I think that this kind of
>> implementation-defined
>> behaviour is a terribly bad idea, eg.:
>>
>> - it breaks common assumptions about code, especially how it behaves
>> under
>> seemingly innocuous refactorings, or with a different set of compiler
>> flags.
>
> As pointed out, this already happens with just about every language. It
> happens with all C/C++ compilers I'm aware of.
I'm not claiming those languages don't have broken floating point
semantics. I have sometimes been using inline assembler in C++ to get
the results I want. It's painful and unnecessary.
> It happens as the default behavior of the x86.
I know. I don't care. It is a stupid idea. See above.
> And as pointed out, refactoring (x+y)+z to x+(y+z)
> often produces different results, and surprises a lot of people.
> ...
As far as I can tell, you are saying: "You think A is bad, but A is
similar to B, and B is bad, but B is hard to fix, hence A is actually
good." I disagree.
For floating point, '+' means: "add precisely, then round". It is indeed
potentially surprising and not very helpful for generic code that
floating point types and integral types use the same syntax for
conceptually different things (i.e., the language commits operator
overloading abuse), but we are stuck with that now.
Implementation-defined behaviour can actually be eliminated in a
backward-compatible way.
Refactorings as simple as moving some expression into its own function
should be possible without surprisingly wreaking havoc. Implementations
can (and do) choose strange and useless behaviours. The fact that types
do not actually specify what kind of value will be used at runtime is a
pointless waste of type safety.
>
>> - it breaks reproducibility, which is sometimes more important that
>> being close
>> to the infinite precision result (which you cannot guarantee with any
>> finite
>> floating point type anyway).
>> (E.g. in a game, it is enough if the result seems plausible, but it
>> should be
>> the same for everyone. For some scientific experiments, the ideal case
>> is to
>> have 100% reproducibility of the computation, even if it is horribly
>> wrong, such
>> that other scientists can easily uncover and diagnose the problem, for
>> example.)
>
> Nobody is proposing a D feature that does not produce reproducible
> results
Do you disagree with the notion that implementation-defined behaviour is
almost by definition detrimental to reproducibility? (I'm not trying to
say that it makes reproducing results impossible. It is a continuum.)
> with the same program on the same inputs.
I'm talking about computations, not just programs, and I ideally want
consistent behaviour across compilers/compiler versions/compiler flags
(at least those flags not specifically designed to change language
semantics in precisely that way). If you can additionally give me
refactorability (i.e. a lack of unprincipled interplay between language
features that should be orthogonal [1]), that would be really awesome.
The result should ideally not depend on e.g. whether a computation is
run at compile-time or run-time, or whatever other irrelevant implicit
detail that is changed during refactoring or when switching machines.
(e.g. someone might be running a 32-bit system, and another person (or
the same person) might be running a 64-bit system, and they get
different floating-point results. It just adds a lot of friction and
pointless work.)
> This complaint is a strawman, as I've pointed out multiple times.
> ...
A strawman is an argument one incorrectly claims that the other party
has made.
Here, I think the underlying problem is that there is a
misunderstanding. (I.e. you think I'm saying something I didn't intend
to say, or vice-versa. [2])
This is often the case, hence I try to avoid calling out strawmen by
name and instead try to put my argument differently. (E.g., when you
seemingly started to claim that my argument was something like "using
lower precision for the entire computation throughout should be expected
to yield more accurate results" and then ridiculed it.)
> In fact, the results would be MORE portable than with C/C++,
I know that it is better than completely broken. I'm sorry, but I have
higher standards. It should be not broken.
> because the
> FP behavior is completely implementation defined, and compilers take
> advantage of that.
>
I guess the reason why it is completely implementation defined is the
desire that there should be completely standard-compliant C/C++
compilers for each platform, and the fact that implementers didn't want
to support IEEE 754 everywhere. One way to implement a specification is
to weaken the specification.
[1] This is the single most important thing programming languages
developed in academia often get right.
[2] To clarify: I know that if I compile a program on systems with
identical states, on an identical compiler with identical flags, I will
(or should) get identical binaries that then produce identical results
on conforming hardware. (See how I didn't need to say "identical
hardware"? I want to be there!) It is not always desirable or practical
to keep around all that additional state though.
More information about the Digitalmars-d
mailing list