3 ms·
I'm not a Java expert, but I'm fairly certain this is just a feature of roundoff error and how it propagates through computations, and how floating-point values
by WCSTombs 2y ago
I'm not a Java expert, but I'm fairly certain this is just a feature of roundoff error and how it propagates through computations, and how floating-point values are represented.
sin(pi/2 + x) = cos(x) = 1 - x^2/2 + {lower-order terms}
When you compute Math.sin(Math.PI/2), Math.PI/2 is computed as pi/2 (exact) plus a small error term, x, which is on the order of 1e-16 or so. Math.sin(Math.PI/2) is then computed as
sin(pi/2 + x),
which is about 1 - x^2/2. But since x is on the order of 1e-16, x^2/2 is on the order of 1e-32, and the closest double-precision value to 1 - x^2/2 can easily be seen to be exactly 1.0.
On the other hand,
sin(2*pi + x) = sin(x) = x + {lower-order terms}
When you compute Math.sin(2*Math.PI), 2*Math.PI is computed as 2*pi (exact) plus again a small error term x which is on the order of 1e-16, and Math.sin(2*Math.PI) is very close to that error term x. The closest double-precision value to x is much closer to x than zero is, because x is already very small and the relative approximation error in its best double-precision approximation is on the order of 1e-16. Thus, you get a small but nonzero result that is very close to the original approximation error of 2*Math.PI versus the mathematical 2*pi.
To fully follow that argument above, you need to know a few things about numerical computation:
- Most (but not all) computational environments implement some version of the IEEE-754 standard, and I'm assuming this is also true of Java.
- IEEE-754 requires that all the basic operations, including trigonometric functions, behave as if the exact result was computed and then rounded to the nearest double-precision value.
- In IEEE-754, floating point numbers are essentially represented in scientific notation, or more precisely as
+/-(1.XXXX...) * 2^E
where the 1.XXXX... is a binary number with a fixed number of binary digits after the binary point, and E is an integer represented using a fixed number of bits. (For double-precision, I believe the XXXX... has 52 bits, and E has 11 bits.)
- shivajikobardan 2y agoI want to study more about how an example processor computes various stuffs like sqrt, sin, multiplication, addition, subtraction, division of floating point numbers. I've studied computer organization books and understood the IEEE754 notation about floating point representation, but I still can't do the calculations of division, sqrt by hand(although I can compute the floating point representation in terms of mantiassa, sign and exponent by myself manually). I know this might sound stupid to hand calculate everything, but that's what gives me chills at night.
- WCSTombs 2y agoDo you mean you want to know how to implement those operations on the hardware level (transistors or logic gates), or that you just want to be able to do all those things by hand? Those are very different questions. I guess I'm not sure what you mean by "example processor."
- shivajikobardan 2y agoI mean I want to do it by hand. I don't want to go transistor level of depth. Just at the level of processors architecture. and algorithm.
- WCSTombs 2y agoThere are a few different ways, so I'll choose a method that's pretty easy to explain, but not necessarily the simplest to actually execute. I'll assume you can do addition, subtraction, and multiplication of binary floating-point numbers by hand. For division A/B, you compute it as A*(1/B), so it suffices to be able to compute 1/B. Because in floating-point you can multiply by powers of 2 and +/-1 "for free," we can assume B is between 1 and 2. So here's the algorithm: x_0 = 3/4 x_{n+1} = x_n*(2 - x_n*B) It can be shown that x_n converges to 1/B, and in fact each subsequent iteration has approximately double the number of correct bits. For square roots, it's a pretty similar idea. To compute sqrt(B), we can similarly assume B is between 0.5 and 2, and then x_0 = 1 x_{n+1} = 0.5*(x_n + B/x_n) (So you need to use the division algorithm as a subroutine.) For sine and cosine, I would start with the Taylor series. You can use the symmetry and periodicity to bring the argument (for example) into [0, pi/2], where you can guarantee a good error bound with a finite number of terms of the Taylor series. There's also a trick, you can divide the argument by some large power of two to make it really close to zero, use just a few terms of the Taylor series to get great approximations to sin(x/2^n) and cos(x/2^n), and then use the angle-doubling trigonometric identities sin(2*a) = 2*sin(a)*cos(a) cos(2*a) = cos(a)^2 - sin(a)^2 repeatedly to get sin(x) and cos(x). These are just some ways to approach these computations, and there are other methods. It's not unreasonable to base computational architecture on these algorithms, but if you really want to do them by hand on paper, there may be simpler ways, especially if you just want a few significant bits or digits.
- WCSTombs 2y agoFor Math.sin(Math.pi/6), I'm not sure. When I run it in Python, I get 0.49999999999999994, not exactly 0.5, so it could be that your implementation is slightly different, or that the printed value doesn't exactly match the in-memory representation.