JDK-8279986 : methods Math::asXExact for safely checked primitive casts
  • Type: Enhancement
  • Component: core-libs
  • Sub-Component: java.lang
  • Priority: P4
  • Status: In Progress
  • Resolution: Unresolved
  • Submitted: 2022-01-13
  • Updated: 2024-05-06
Related Reports
CSR :  
Duplicate :  
Relates :  
Relates :  
Description
When Java programmers work with primitives they often have to use truncating casts to force an expression value into a variable of a smaller type.  This is common with byte and short variables, but can also happen with converting a long result to an int variable, or a floating point value, perhaps the output of a scaling expression, to a fixed point value.  (Also double to float and even long to float.)

In all of these cases the tool of choice is a Java cast, but there is a problem:  Casts throw away information.  (They truncate range.  Also sometimes precision, as with int to float and long to double.)  A programmer consciously writing robust code, perhaps with security in mind, needs to check that truncation has not occurred, but there is no good way to do this.

By "good way to do this" we mean not only something easy to read and debug, but also known to the JIT and properly optimized.

There is already a function for the specific case of narrowing long to int, `Math::toIntExact`.  This RFE calls for (i) extending the pattern to other casts that may lose information, and (ii) making the existing `toIntExact` function an intrinsic to ensure proper optimization. 

Specific API points, all in java.lang.Math:

```
toIntExact(long)  [ensure existing method is properly optimized]
toIntExact(double) : int
toShortExact(long | double) : short
toByteExact(long | double) : byte
toFloatExact(long | double) : float
toDoubleExact(long) : double
toLongExact(double) : long
toUnsignedIntExact(long | double) : long (note wider return types for unsigned)
toUnsignedShortExact(long | double) : int
toUnsignedByteExact(long | double) : int
```

It should be the case that C2 code for these is efficient.  A normal cast is just an instruction or two, and the code for these new methods (as well as existing `toIntExact`) should use about the same number of instructions, plus (in most cases) an uncommon trap for the exception paths.

Getting the code right might require marking these as intrinsics (which was the original formulation of this RFE).  But, as Andrew Haley points out quite correctly, intrinsics are often overkill, since they (in their usual form) require special logic in C2 (see the file `libraryCall.cpp` and even new special IR node types (cf. `MinLNode` etc.) and corresponding AD file definitions and/or lowering logic.

The above methods can probably be made to emit excellent code by means of up to two special techniques available to JDK code: 1. marking them `@ForceInline` (which is an annotation private to the JDK) and 2. using an idiom that C2 knows to speculate with an uncommon trap (which depends on the details of Java code shape).

A good idiom for ensuring that C2 produces proper fast paths is to make it absolutely clear (to C2) which are the exception paths, while factoring out (into a helper method) the seldom-used code which creates the actual exception object.  This means that a failed check should have an explicit `throw` statement, but the exception thrown by the throw statement should be created by a helper routine.  (The helper might as well throw it; that doesn't matter; the point is that C2 statically sees a throw instead of a possibly opaque method call or a bunch of exception creation logic.)

Semantics:  Each method has 1. an input type A, 2. a value set to check against V, and 3. an output type R.  These three items are straightforwardly encoded in the method names and types given above.

R and A must both be wide enough to represent the full range of V.

If V is the value set of a Java type T then R=T.

A must contain values not in V, which are to be rejected.

If R or A is floating precision in V may be lost; that must be checked for and rejected.

In the unsigned cases, V corresponds to a Java type T, and the method should behave like this:

```
public static R toVExact(A value) {
   R result = (T) value;
   if (result != value || value == BADV)
      throw newInexactException(…);
   return result;
}
```

When exactly one of R,T is a floating point type, an additional check against `BADV` is needed to detect positive overflow, since (long)(double)Long.MAX_VALUE==Long.MAX_VALUE but the floating point representation is not exactly that value and similarly for int versus float.  The bad value is one of `Long.MAX_VALUE`, `Integer.MAX_VALUE`, `0x1p63`, or `0x1p31f`.  There is no such check for other combinations of types.  (Hat tip to Raffaello Giulietti for this insight.)

For the unsigned cases V is a range `[0..(1L<<C.SIZE)-1]` where C is the signed Java type corresponding to V.  Then A is a Java type whose range contains the range of C, plus the values in V (so it is wider than C).  In that case the method should behave like this:

```
public static R toVExact(A value) {
   R result = (long) value & ((1L<<C.SIZE)-1);
   if (result != value)
      throw newInexactException(…);
   return result;
}
```

The unsigned variations have to return a wider type.

Some use cases will employ "sign punning" on types, using signed types to carry unsigned values, as with byte variables which logically carry values in the unsigned range `[0..0xFF]` but physically carry the signed range `[-128..127]`.  Those use cases will typically have extra down-casts, such as `byte x = (byte)toUnsignedByteExact(x)`.  Such a sign-punning cast is does not destroy logical values, and is evidence that the programmer certifies the variable's type to be legitimate.  This API provides no special help for sign-punning.

Comments
A pull request was submitted for review. URL: https://git.openjdk.java.net/jdk/pull/8548 Date: 2022-05-05 10:11:05 +0000
05-05-2022

OK, but one thing bothers me. I'm a little bemused that we frequently seem to reach for intrinsics as a way to ensure efficient code. toIntExact(long), for example, probably requires three instructions: truncate, compare, branch. C2 is likely to generate efficient code for this, intrinsic or no intrinsic. Perhaps we should only create intrinsics where benchmarking shows that doing so will have a useful effect on Java code.
04-02-2022