August 24, 2015
On Monday, 24 August 2015 at 18:16:44 UTC, Justin Whear wrote:
> On Mon, 24 Aug 2015 18:06:07 +0000, rumbu wrote:
>
>> BTW, 1.2 and 12.0 are directly representable as double
>> 
>> In C++:
>> 
>> printf("%.20f\r\n", 1.2);
>> printf("%.20f\r\n", 12.0);
>> 
>> will output:
>> 
>> 1.20000000000000000000 12.00000000000000000000
>> 
>> Either upcasting to real is the wrong decision here, either the writeln string conversion is wrong.
>
> No it's not, this must be some sort of constant-folding or precision increase.

Maybe the constant folding is using a different rounding mode to the runtime?


August 24, 2015
On 8/24/15 2:06 PM, rumbu wrote:
> BTW, 1.2 and 12.0 are directly representable as double
>
> In C++:
>
> printf("%.20f\r\n", 1.2);
> printf("%.20f\r\n", 12.0);
>
> will output:
>
> 1.20000000000000000000
> 12.00000000000000000000
>
> Either upcasting to real is the wrong decision here, either the writeln
> string conversion is wrong.

I don't think they are directly representable as floating point, because they are have factors other than 2 in the decimal portion. From my understanding, anything that only has to do with powers of 2 are representable in floating point, just like you cannot represent 1/3 in decimal exactly.

But there is definitely something weird going on with the casting.

I wrote this program:

testfp.d:
extern(C) void foo(double x);
void main() {
    double x = 1.2;
    foo(x);
}

testfp2.d:
extern(C) void foo(double x)
{
    import std.stdio;
    writeln(cast(ulong)(x * 10.0));
}

testfp2.c:
#include <stdio.h>

void foo(double x)
{
    printf("%lld\n", (unsigned long long)(x * 10));
}


If I link testfp.d against testfp2.c, then it outputs 12. If I link against testfp2.d, it outputs 11.

I have faith that printf and writeln properly output ulongs. Something different happens with the cast. There can be no constant folding operations or optimizations going on here, as this is done via separate compilation. I'll re-open the bug report.

-Steve
August 24, 2015
On 8/24/15 2:38 PM, Steven Schveighoffer wrote:
> On 8/24/15 2:06 PM, rumbu wrote:
>> BTW, 1.2 and 12.0 are directly representable as double
>>
>> In C++:
>>
>> printf("%.20f\r\n", 1.2);
>> printf("%.20f\r\n", 12.0);
>>
>> will output:
>>
>> 1.20000000000000000000
>> 12.00000000000000000000
>>
>> Either upcasting to real is the wrong decision here, either the writeln
>> string conversion is wrong.
>
> I don't think they are directly representable as floating point, because
> they are have factors other than 2 in the decimal portion. From my
> understanding, anything that only has to do with powers of 2 are
> representable in floating point, just like you cannot represent 1/3 in
> decimal exactly.
>
> But there is definitely something weird going on with the casting.
>
> I wrote this program:
>
> testfp.d:
> extern(C) void foo(double x);
> void main() {
>      double x = 1.2;
>      foo(x);
> }
>
> testfp2.d:
> extern(C) void foo(double x)
> {
>      import std.stdio;
>      writeln(cast(ulong)(x * 10.0));
> }
>
> testfp2.c:
> #include <stdio.h>
>
> void foo(double x)
> {
>      printf("%lld\n", (unsigned long long)(x * 10));
> }
>
>
> If I link testfp.d against testfp2.c, then it outputs 12. If I link
> against testfp2.d, it outputs 11.
>

More data:

It definitely has something to do with the representation of 1.2 * 10.0 in *real*.

I changed the code so that it writes the result of the multiplication to a shared double.

In this case it *works* and prints 12, just like C does.

This also works:

double x = 1.2;
double y = x * 10.0;
writeln(cast(ulong)y); // 12

However, change y to a real, and you get 11.

Note that if I first convert from real to double, then convert to ulong, it works.

This code:

    double x = 1.2;
    double x2 = x * 10.0;
    real y = x * 10.0;
    real y2 = x2;
    double y3 = y;
    writefln("%a, %a, %a", y, y2, cast(real)y3);


outputs:

0xb.ffffffffffffep+0, 0xcp+0, 0xcp+0

So some rounding happens in the conversion from real to double, that doesn't happen in the conversion from real to ulong.

All this gets down to: FP cannot accurately represent decimal. Should this be fixed? Can it be fixed? I don't know. But I would be very cautious about converting anything FP to integers without some epsilon.

-Steve
August 24, 2015
On Monday, 24 August 2015 at 18:59:58 UTC, Steven Schveighoffer wrote:
> All this gets down to: FP cannot accurately represent decimal. Should this be fixed? Can it be fixed? I don't know. But I would be very cautious about converting anything FP to integers without some epsilon.
>
> -Steve

I don't see anything that needs to be fixed, because I don't think anything is broken - there is nothing that violates my understanding of floating point precision in D.

cast is not round. What is broken is a program that attempts to convert a double to an integer type using a cast rather than the functions that were written to do it correctly.
August 24, 2015
On 8/24/15 3:15 PM, bachmeier wrote:
> On Monday, 24 August 2015 at 18:59:58 UTC, Steven Schveighoffer wrote:
>> All this gets down to: FP cannot accurately represent decimal. Should
>> this be fixed? Can it be fixed? I don't know. But I would be very
>> cautious about converting anything FP to integers without some epsilon.
>>
>> -Steve
>
> I don't see anything that needs to be fixed, because I don't think
> anything is broken - there is nothing that violates my understanding of
> floating point precision in D.
>
> cast is not round. What is broken is a program that attempts to convert
> a double to an integer type using a cast rather than the functions that
> were written to do it correctly.

What is surprising, and possibly buggy, is that none of these operations involve real, but the issue only happens because under the hood, real is used instead of double for the multiplication.

I pretty much agree with you that the code is written incorrectly. But it is unfortunate it differs in the way it handles this from C. I think this issue has been brought up before on the newsgroup, especially where CTFE is involved.

-Steve
August 24, 2015
On Mon, Aug 24, 2015 at 07:15:43PM +0000, bachmeier via Digitalmars-d wrote:
> On Monday, 24 August 2015 at 18:59:58 UTC, Steven Schveighoffer wrote:
> >All this gets down to: FP cannot accurately represent decimal. Should this be fixed? Can it be fixed? I don't know. But I would be very cautious about converting anything FP to integers without some epsilon.
> >
> >-Steve
> 
> I don't see anything that needs to be fixed, because I don't think anything is broken - there is nothing that violates my understanding of floating point precision in D.
> 
> cast is not round. What is broken is a program that attempts to convert a double to an integer type using a cast rather than the functions that were written to do it correctly.

+1.

Floating-point != mathematical real numbers. Don't expect it to behave the same.


T

-- 
It's amazing how careful choice of punctuation can leave you hanging:
August 24, 2015
On 8/24/15 1:43 PM, bachmeier wrote:
> On Monday, 24 August 2015 at 16:52:54 UTC, Márcio Martins wrote:
>> I'm posting this here for visibility. This was silently corrupting our
>> data, and might be doing the same for others as well.
>>
>> import std.stdio;
>> void main() {
>>   double x = 1.2;
>>   writeln(cast(ulong)(x * 10.0));
>>   double y = 1.2 * 10.0;
>>   writeln(cast(ulong)y);
>> }
>>
>> Output:
>> 11
>> 12
>>
>>
>> to!ulong instead of the cast does the right thing, and is a viable
>> work-around.
>>
>> Issue: https://issues.dlang.org/show_bug.cgi?id=14958)
>
> I would not describe to!ulong as a "work-around". You just discovered
> one of the reasons to! exists: it is the right way to do it and
> cast(ulong) is the wrong way. As the others have noted, floating point
> is tricky business, and you need to use the right tools for the job.

real y = x * 10.0;
writeln(y.to!ulong); // 11

to! does not do anything different than cast. What is happening here is the implicit cast from real to double. D treats the result of x * 10.0 as type double, but it's done at real precision. In that conversion, the error is hidden by a rounding automatically done by the processor I think.

-Steve
August 24, 2015
On Monday, 24 August 2015 at 19:23:44 UTC, Steven Schveighoffer wrote:

>
> real y = x * 10.0;
> writeln(y.to!ulong); // 11
>
> to! does not do anything different than cast. What is happening here is the implicit cast from real to double. D treats the result of x * 10.0 as type double, but it's done at real precision. In that conversion, the error is hidden by a rounding automatically done by the processor I think.
>
> -Steve

Yes, I was mistaken. You have to use roundTo or std.math.round. to! and cast both truncate.
August 24, 2015
On Monday, 24 August 2015 at 19:23:44 UTC, Steven Schveighoffer wrote:
> On 8/24/15 1:43 PM, bachmeier wrote:
>> On Monday, 24 August 2015 at 16:52:54 UTC, Márcio Martins wrote:
>>> I'm posting this here for visibility. This was silently corrupting our
>>> data, and might be doing the same for others as well.
>>>
>>> import std.stdio;
>>> void main() {
>>>   double x = 1.2;
>>>   writeln(cast(ulong)(x * 10.0));
>>>   double y = 1.2 * 10.0;
>>>   writeln(cast(ulong)y);
>>> }
>>>
>>> Output:
>>> 11
>>> 12
>>>
>>>
>>> to!ulong instead of the cast does the right thing, and is a viable
>>> work-around.
>>>
>>> Issue: https://issues.dlang.org/show_bug.cgi?id=14958)
>>
>> I would not describe to!ulong as a "work-around". You just discovered
>> one of the reasons to! exists: it is the right way to do it and
>> cast(ulong) is the wrong way. As the others have noted, floating point
>> is tricky business, and you need to use the right tools for the job.
>
> real y = x * 10.0;
> writeln(y.to!ulong); // 11
>
> to! does not do anything different than cast. What is happening here is the implicit cast from real to double. D treats the result of x * 10.0 as type double, but it's done at real precision. In that conversion, the error is hidden by a rounding automatically done by the processor I think.
>
> -Steve

Whatever the issue is, it is not unavoidable, because as has been shown, other languages do it correctly.

From the data presented so far, it seems like the issue is that the mul is performed in 80-bit precision, storing it before the cast forces a truncation down to 64-bit. Similarly, passing it to a function will also truncate to 64-bit, due to ABIs. This is why to! works as expected.

Please do keep in mind that the issue is not one of precision, but one of inconsistency. They are not the same thing. The result being 11 or 12 is irrelevant to this issue. It should just be the same for two instances of the same expression.

In an attempt to make things more obvious, consider this example, which also illustrates why to! works, despite apparently doing nothing extra at all.

double noop(double z) {
  return z;
}

void main() {
  double x = 1.2;
  writeln(cast(ulong)(x * 10.0));
  writeln(cast(ulong)noop(x * 10.0));
}

Outputs:
11
12
August 24, 2015
On 8/24/15 4:34 PM, "=?UTF-8?B?Ik3DoXJjaW8=?= Martins\" <marcioapm@gmail.com>\"" wrote:
> On Monday, 24 August 2015 at 19:23:44 UTC, Steven Schveighoffer wrote:
>> On 8/24/15 1:43 PM, bachmeier wrote:
>>> On Monday, 24 August 2015 at 16:52:54 UTC, Márcio Martins wrote:
>>>> I'm posting this here for visibility. This was silently corrupting our
>>>> data, and might be doing the same for others as well.
>>>>
>>>> import std.stdio;
>>>> void main() {
>>>>   double x = 1.2;
>>>>   writeln(cast(ulong)(x * 10.0));
>>>>   double y = 1.2 * 10.0;
>>>>   writeln(cast(ulong)y);
>>>> }
>>>>
>>>> Output:
>>>> 11
>>>> 12
>>>>
>>>>
>>>> to!ulong instead of the cast does the right thing, and is a viable
>>>> work-around.
>>>>
>>>> Issue: https://issues.dlang.org/show_bug.cgi?id=14958)
>>>
>>> I would not describe to!ulong as a "work-around". You just discovered
>>> one of the reasons to! exists: it is the right way to do it and
>>> cast(ulong) is the wrong way. As the others have noted, floating point
>>> is tricky business, and you need to use the right tools for the job.
>>
>> real y = x * 10.0;
>> writeln(y.to!ulong); // 11
>>
>> to! does not do anything different than cast. What is happening here
>> is the implicit cast from real to double. D treats the result of x *
>> 10.0 as type double, but it's done at real precision. In that
>> conversion, the error is hidden by a rounding automatically done by
>> the processor I think.
>
> Whatever the issue is, it is not unavoidable, because as has been shown,
> other languages do it correctly.

Your other examples use doubles, not reals. It's not apples to apples.

>  From the data presented so far, it seems like the issue is that the mul
> is performed in 80-bit precision, storing it before the cast forces a
> truncation down to 64-bit.

Not just truncation, rounding too.

> Similarly, passing it to a function will also
> truncate to 64-bit, due to ABIs. This is why to! works as expected.
>
> Please do keep in mind that the issue is not one of precision, but one
> of inconsistency.

It is an issue of precision. In order to change from real to double, some bits must be lost. Since certain numbers cannot be represented, the CPU must round or truncate.

> They are not the same thing. The result being 11 or 12
> is irrelevant to this issue. It should just be the same for two
> instances of the same expression.

They are not the same expression. One goes from double through multiplication to real, then back to double, then to ulong. The other skips the real to double conversion and goes directly to ulong.

The real issue here is that you are not correctly converting from a floating point number to an integer.

> In an attempt to make things more obvious, consider this example, which
> also illustrates why to! works, despite apparently doing nothing extra
> at all.
>
> double noop(double z) {
>    return z;
> }
>
> void main() {
>    double x = 1.2;
>    writeln(cast(ulong)(x * 10.0));
>    writeln(cast(ulong)noop(x * 10.0));
> }
>
> Outputs:
> 11
> 12

I understand the inconsistency, and I agree it is an issue that should be examined. But the issue is entirely avoidable by not using incorrect methods to convert from floating point to integer after floating point operations introduce some small level of error.

Perhaps there is some way to make it properly round in this case, but I guarantee it will not fix all floating point errors.

-Steve