33

Java was released under the slogan 'write once, run anywhere'; while its adoption was probably more about 'now we have a language that provides garbage collection in a familiar workflow and with a good standard library', as far as Sun's management was concerned, it was an anti-Microsoft weapon, and to this end, it tried to pave over differences between platforms.

One category of differences was in floating-point arithmetic. IEEE 754 made some progress towards reproducible floating point, but still left a great deal of variation. Java tried to nail it down. This was a controversial move. William Kahan wrote an impassioned rant about it; as it happens, I mostly agree with the Java designers and mostly disagree with Kahan, but his writing is always thought-provoking.

Now I vaguely remember the Java decision mostly held up, and to this day floating point in Java is mostly reproducible, but there was one issue, one small aspect of floating-point arithmetic, on which they backed down, one edge case in which platform differences in floating-point hardware can still manifest. But I cannot for the life of me remember what that was.

What was the aspect of floating point on which a later version of Java backed down?

4
  • This is a great question, but I feel like it's off-topic for this particular Stack. Maybe SoftwareEngineering.SE would be a better fit?
    – Ian Kemp
    Commented Mar 1, 2021 at 17:26
  • 3
    @IanKemp Why do you "feel like it's off-topic for this particular Stack"? It seems to me this question is about a design decision that was made some time in the past, and that now for better or for worse has become part of "computing history". That's well within scope of this site, no?
    – Will
    Commented Mar 2, 2021 at 8:20
  • 1
    @Will There's little that's "retro" about Java.
    – Ian Kemp
    Commented Mar 2, 2021 at 8:52
  • 2
    @IanKemp Eh, I'd argue that Java is already seen as a somewhat old-fashioned language in some regards. Besides, whether or not you consider it retro will depend on how old you are, and maybe how many years into the future you read this comment.
    – Will
    Commented Mar 2, 2021 at 9:19

3 Answers 3

35

The range of intermediate results.

The Java Language Specification, 2nd Ed. relaxed the evaluation rules for floating-point expressions by introducing the notion of an ‘FP-strict’ expression, defined as follows (§15.4, p. 319):

Within an FP-strict expression, all intermediate values must be elements of the float value set or the double value set, implying that the results of all FP-strict expressions must be those predicted by IEEE 754 arithmetic on operands represented using single and double formats. Within an expression that is not FP-strict, some leeway is granted for an implementation to use an extended exponent range to represent intermediate results; the net effect, roughly speaking, is that a calculation might produce “the correct answer” in situations where exclusive use of the float value set or double value set might result in overflow or underflow.

The actual semantics are detailed in §5.1.8 ‘Value Set Conversion’; the two sections are then referred to in other sections that specify what the specification considers ‘intermediate’ floating-point results (which is basically boils down to all arithmetic operations, but not variable assignment or argument passing).

The FP-strict mode can be enabled by the strictfp keyword, which may be applied to classes (JLS 2nd Ed., §8.1.1.3), methods (§8.4.3.5) and interfaces (§9.1.1.2).

This definition has remained unchanged in the specification as late as in Java 15 (JLS 15, §15.4 and §5.1.13 respectively). In the first edition, no such ‘strictness’ notion is present and thus all floating-point expressions are implicitly strict.

This is basically the same issue that the C language has (see GCC’s infamous bug 323 and the ‘Disappointments’ section of the GCC manual); though in Java’s case, the effects of this are considerably less wild as the specification only allows the JVM implementation to extend the exponent range and mandates that values be rounded strictly to their nominal type at variable assignment (JLS 2nd Ed. §5.2) and argument passing (§5.3).

The other answers (@gnasher729’s and @supercat’s) go into more detail as to the motivation; the latter also made me realise I have misread the specification slightly.

2
12

From what I understand, unless things have changed, I think that even in non-strict mode, Java required that floating-point values be rounded to the appropriate length in the mantissa. Leeway was allowed, however, with values that were smaller the smallest normalized float value. Most float values should have 24 bits of precision, but values whose magnitude is between 0.5 and 1.0 times the smallest normalized value should have 23 bits, those whose magnitude is between 0.25 and 0.5 times the smallest normalized value should have 22 bits, etc. Many hardware platforms had ways of quickly rounding a mantissa to 24 bits, but couldn't efficiently force the magnitude-dependent rounding except by actually storing a value to memory as a float and then reading it back.

A lot of the controversy surrounding such issues could be avoided if languages were to provide different floating-point types which had matching representations but different semantics, and a way of specifying which particular type should be represented by keywords like float or double. If general-purpose implementations are expected to provide modes to explicitly select the language-default behaviors, as well as modes which use whichever behaviors are most efficient, then someone who wishes to write code which works efficiently but correctly on a wide range of platforms could test their code using both the mode that would run more efficiently on their platform, and a mode that would run much less efficiently on their platform but would mimic the behavior of other platforms. Unfortunately, the normal tendency has been to try to push for all implementations to process everything the same way, despite the fact that different behaviors will be more useful for different platforms and purposes.

12

The biggest problem was that on Intel CPUs with external floating point unit (80387 IIRC) all operations were done in extended precision, while Java required double precision. And due to “double rounding”, you can’t use an extended precision operation and round to double precision, in rare situations this would change the result.

So this would have been a total performance disaster. But fortunately that FPU can be set into a “64 bit rounding” mode, where all results have their mantissa rounded to double precision. There was a problem left that wasn’t quite as bad: Even with 64 bit rounding, the exponent range was the full extended precision range. So calculating (a * b) / c should produce an infinity if a, b and c are very large, but gave the correct result instead. Workarounds would have been expensive.

As a result, Java allowed operations to have temporary results calculated at higher than double precision. This still required rounding when a result was assigned to a variable, but that was just about bearable. The problem is gone with SSE floating point.

4
  • 7
    IIRC the Intel 80 bits FP format dates back to the original 8087, and carried over to CPU's with an internal FP unit as well (486/Pentium).
    – MSalters
    Commented Mar 1, 2021 at 10:23
  • @MSalters: Machines without floating-point units could also process the 80-bit format significantly more efficiently than the 64-bit format, despite the fact that it offered higher precision.
    – supercat
    Commented Mar 1, 2021 at 18:15
  • 2
    @MSalters: That's correct, the x87 internal format has always been 80-bit. We only stopped using it when we started doing scalar FP in XMM registers, with SSE1/2 instructions. (The default for x86-64 ABIs, and sometimes used in 32-bit code these days, too.) However, the mantissa precision can be reduced with a per-thread FP control register setting, and some Windows C/C++ environments did this by default to make x87 more like double or float, except for the exponent range: randomascii.wordpress.com/2012/03/21/…. (And I guess JVMs everywhere) Commented Mar 2, 2021 at 13:03
  • Nit: it’s the range of intermediate results that differs from standard 64-bit floats; the ‘precision’ (which only depends on the number of bits of the significand) remains the same. Otherwise, I think this is the clearest explication of this change’s motivation. Commented Mar 5, 2021 at 17:22

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .