4

I have an unit test failing on a Math.Tan(-PI/2) returning the wrong version in .NET.

The 'expected' value is taken from Wolfram online (using the spelled-out constant for -Pi/2). See for yourselves here.

As correctly observed in the comments, the mathematical result of tan(-pi/2) is infinity. However, the constant Math.PI does not perfectly represent PI, so this is a 'near the limit' input.

Here's the code.

double MINUS_HALF_PI = -1.570796326794896557998981734272d;
Console.WriteLine(MINUS_HALF_PI == -Math.PI/2); //just checking...

double tan = Math.Tan(MINUS_HALF_PI);
Console.WriteLine("DotNET  {0:E20}", tan);

double expected = -1.633123935319534506380133589474e16;
Console.WriteLine("Wolfram {0:E20}", expected);

double off = Math.Abs(tan-expected);
Console.WriteLine("         {0:E20}", off);

This is what gets printed:

True
DotNET  -1.63317787283838440000E+016
Wolfram -1.63312393531953460000E+016
         5.39375188498000000000E+011

I thought it's an issue of floating-point representation.

Strangely though, the same thing in Java DOES return the same value as Wolfram, down to the last digit - see it evaluated in Eclipse. (The expressions are cropped - you'll have to believe me they use the same constant as MINUS_HALF_PI above.)

enter image description here

True
DotNET  -1.63317787283838440000E+016
Wolfram -1.63312393531953460000E+016
Java    -1.63312393531953700000E+016

As you can see, the difference is:

  • between Wolfram and .NET: ~5.39 * 10^11
  • between Wolfram and Java: =2.40 * 10^1

That's ten orders of magnitude!

So, any ideas why the .NET and Java implementations differ so much? I would expect them both to just defer the actual computing to the processor. Is this assumption unrealistic for x86?

Update

As requested, I tried running in Java with strictfp. No change:

enter image description here

9
  • First, Math.Tan() only takes a double as input. Second, I don't think decimal can help in this regard. Third, Java and .NET use the same standard representation for doubles so the ability to represent the value is unlikely to be the problem. Commented Nov 29, 2013 at 14:22
  • you´r right, Math.Tan cannot work with decimal Commented Nov 29, 2013 at 14:23
  • Have you tried running with strictfp? Commented Nov 29, 2013 at 14:25
  • 5
    Unless I've forgotten all my trigonometry, tan(-Pi/2) is +/- infinity. Figures that different implementations would behave unpredictably around that area when rounding errors start flying around. I wonder what exactly differs between the implementations to cause these different results, though. Commented Nov 29, 2013 at 14:29
  • 1
    Possible related: extremeoptimization.com/Blog/index.php/2011/02/…
    – nemesv
    Commented Nov 29, 2013 at 14:30

1 Answer 1

6

The entire question is constructed to create a tendentious result. The double value closest to half PI is -1.5707963267948966; the other digits are just ignored. So it’s no wonder that neither C# nor Java detect that the remaining 14 more digits are not turning the result closer to -PI/2, but carefully chosen to trick Wolfram Alpha to return a value close to the result of Java.

-1.570796326794896557998981734272 // the number from the question
-1.57079632679489661923132169163975… // the real digits of -PI/2
                  ↑
                the end of the double precision

Any other number within the range that would get rounded to the same double number including the exact double value as used by Java yields to a value on Wolfram Alpha having nothing in common with neither, the C# nor Java result.

7
  • Nice catch. Be interesting to see what the OP says! Commented Nov 29, 2013 at 16:44
  • OP says, interesting! It did cross my mind that that constant has way too many digits, but tested it against Math.PI/2 and surprise (not!), they match (in the binary64 representation of double, that is...). It didn't occur to me that the 'useless' extra digits will make a difference in Wolfram. I don't really know how that super-long constant came into existence (I needed to port existing java code to C#) - but it really looks like it was reverse-engineered! :) I'll have a look in the source code history on Monday and post an update if I find anything interesting. Commented Nov 29, 2013 at 21:32
  • As a side question, given a double, how would I get the range of (real numbers) values that would approximate (round off) to that double? I'm thinking of adding and subtracting '1' from the value's mantissa binary representation and use those values as the (exclusive) boundaries of the interval. But maybe there's a simpler way than staring at bits? Commented Nov 29, 2013 at 21:37
  • 1
    In Java, Math.ulp(value) returns the distance to the next double. So basically value±ulp would be that range which gets mapped to value. But, of course, this range cannot get evaluated using double values.
    – Holger
    Commented Nov 29, 2013 at 21:42
  • Nothing similar seems to be built-in in the .NET BCL, but here is a simple way of getting the same result. It's along the lines of my previous comment. Commented Nov 29, 2013 at 22:20

Not the answer you're looking for? Browse other questions tagged or ask your own question.