- If there is an algorithm problem: please realize the absolute value function.
I believe that most people even despise it, (and cast ridicule at it? Here? Here? Can this also be a question?) the following answers are given in less than a minute. If your first impression is different from the following, please leave me a message in the comment area.
// V1.0 public static double myAbs(double value) { if (value < 0) { return -value; } return value; }
At first glance, there is no problem. It conforms to the mathematical definition of absolute value: the absolute value of positive number or zero is itself; The absolute value of a negative number is its opposite. But we ignore a very important thing. The floating-point number in the computer itself uses discrete potential to simulate mathematical continuity. In other words, as programmers, we must consider how to realize the number in the computer, otherwise it will be difficult to explain. Refer to the following codes:
// Read the code and think about whether the program will output oops1 or oops2? double x = -0.0; if (1 / Math.abs(x) < 0) { System.out.println("oops1"); } if (1 / myAbs(x) < 0) { System.out.println("oops2"); }
I won't say the answer. If the absolute value of our implementation is consistent with the behavior of JDK implementation, there should be no output. Please verify it yourself.
Now it's time to face up. The answer given in minutes fails in front of this test case orz. When implementing floating-point numbers, Java refers to IEEE 745 standard. Java distinguishes between positive zero + 0.0 and negative zero - 0.0. There are differences between them when calculating, as shown in the following code.
// Infinity System.out.println(1 / +0.0); // -Infinity System.out.println(1 / -0.0);
But when comparing, there is no difference between the two. This is why the trivial absolute value function we implemented failed in the above test case. In the absolute value function, - 0.0 should not appear, but myAbs returned - 0.0.
System.out.println(+0.0 > 0); // false System.out.println(+0.0 < 0); // false System.out.println(+0.0 == 0); // true System.out.println(-0.0 < 0); // false System.out.println(-0.0 < 0); // false System.out.println(-0.0 == 0); // true System.out.println(-0.0 == +0.0); // true
Now that we have finally figured out the bug, the following is to fix it. A very simple idea is to judge it since it is a problem caused by - 0.0, so the code becomes as follows in less than a minute:
// V1.1 public static double myAbs(double value) { if (value < 0 || -0.0 == value) { return -value; } return value; }
If you think so, congratulations on your second slap in the face, - 0.0 = = + 0.0 is true, and the behavior of myAbs is still incorrect when x=+0.0 in the use case. Fortunately, you can use Double.compare provided by JDK to compare floating-point numbers, Get the following code, and this code can work normally through the test case.
// V1.2 public static double myAbs(double value) { if (value < 0 || Double.compare(value, -0.0) == 0) { return -value; } return value; }
Most people stop here, but if we are the writers of JDK, it is unacceptable to give users such code, because such a commonly used method loses a lot of performance due to the infrequent use case of - 0.0 alone. Positive numbers need to be compared twice, and - 0.0 and + 0.0 need to be compared three times. You can refer to the JDK code source code:
public static int compare(double d1, double d2) { if (d1 < d2) return -1; // Neither val is NaN, thisVal is smaller if (d1 > d2) return 1; // Neither val is NaN, thisVal is larger // Cannot use doubleToRawLongBits because of possibility of NaNs. long thisBits = Double.doubleToLongBits(d1); long anotherBits = Double.doubleToLongBits(d2); return (thisBits == anotherBits ? 0 : // Values are equal (thisBits < anotherBits ? -1 : // (-0.0, 0.0) or (!NaN, NaN) 1)); // (0.0, -0.0) or (NaN, !NaN) }
Double.doubleToLongBits is mainly used to convert floating-point numbers into 8-bit positive Long numbers for comparison. Then we remove the comparison part and add the following logic to myAbs to get the following version. For positive numbers and all zeros, we only need to compare them twice. In the Java JIT, the performance of Double.doubleToLongBits method is ignored, It is just equivalent to reinterpreting the bit in the register. The possible operation is to convert the register specially used for floating-point calculation into a general-purpose register, which is not concerned by the CPU. Therefore, the absolute value calculation of this version is still very fast.
// V1.3 private static final long MINUS_ZERO_LONG_BITS = Double.doubleToLongBits(-0.0); public static double myAbs(double value) { if (value < 0 || Double.doubleToLongBits(value) == MINUS_ZERO_LONG_BITS) { return -value; } return value; }
At present, we have taken another step forward, but there are two branches in the code, which means bad. If the branch prediction of CPU fails, the above code still has performance loss, and we should find ways to reduce branches. At this time, we can refer to the JDK code and find that there is only one branch!
// V1.4 for JDK1.8 public static double abs(double a) { return (a <= 0.0D) ? 0.0D - a : a; }
This can be done based on the fact that whether it is + 0.0 or - 0.0, subtracting with 0.0 will get 0.0, so that the absolute value function will not appear - 0.0 and contains only one branch.
System.out.println(0.0-(-0.0)); // 0.0 System.out.println(0.0-(+0.0)); // 0.0
Is this the ultimate?
NO, by observing IEEE 745, it is found that the first bit in the binary representation of floating-point numbers is the sign bit, 1 represents a negative number and 0 represents a positive number. This is probably the most important bit, because without this bit, you will not be able to distinguish between positive and negative numbers. However, from the operation of calculating the absolute value, the absolute value of the negative number is obtained by removing the sign bits and interpreting the remaining bits as positive numbers. Therefore, the following code is used to obtain the bit representation of the negative number, then the sign position is 0 through the mask 0x7fffffffffffl, and finally reinterpret the obtained value as a floating point number to obtain an absolute value function without branches:
// V1.5 public static double abs(double value) { return Double.longBitsToDouble( Double.doubleToRawLongBits(value) & 0x7fffffffffffffffL); }
In most cases, due to the excellent performance of the Java compiler, the above code has almost no significant performance improvement, but the performance has been improved by about 10% on some platforms. The above code has been submitted to openjdk commit , the article introduces a new absolute value calculation method, which will be updated in Java 18 https://github.com/openjdk/jdk/pull/4711.
Summary:
As a programmer who often writes business code, he generally does not consider these problems. Programmers with computer science background have learned the course of computer composition principle in school, but it is still difficult for you to write a standard absolute value function by yourself. You need to understand the Java compiler, the underlying computer and relevant standards. No matter whether this code will be integrated into openjdk or not, the V1.5 code is only two lines, which is enough to show skills and still admirable. I hope I can also become a programmer who pursues perfection and elegance.
reference resources:
[1] https://en.wikipedia.org/wiki/IEEE_754-1985