Float16.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.arrow.memory.util;
import org.apache.arrow.util.VisibleForTesting;
/**
* Lifted from Apache Parquet MR project:
* https://github.com/apache/parquet-mr/blob/e87b80308869b77f914fcfd04364686e11158950/parquet-column/src/main/java/org/apache/parquet/schema/Float16.java
*
* <ul>
* Changes made:
* <li>Modify the data type input from Parquet-MR Binary (toFloat(Binary b)) to Arrow Java short
* (toFloat(short b))
* <li>Expose NAN and POSITIVE_INFINITY variables
* </ul>
*
* The class is a utility class to manipulate half-precision 16-bit <a
* href="https://en.wikipedia.org/wiki/Half-precision_floating-point_format">IEEE 754</a> floating
* point data types (also called fp16 or binary16). A half-precision float can be created from or
* converted to single-precision floats, and is stored in a short data type. The IEEE 754 standard
* specifies an float16 as having the following format:
*
* <ul>
* <li>Sign bit: 1 bit
* <li>Exponent width: 5 bits
* <li>Significand: 10 bits
* </ul>
*
* <p>The format is laid out as follows:
*
* <pre>
* 1 11111 1111111111
* ^ --^-- -----^----
* sign | |_______ significand
* |
* -- exponent
* </pre>
*
* Half-precision floating points can be useful to save memory and/or bandwidth at the expense of
* range and precision when compared to single-precision floating points (float32). Ref:
* https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/libcore/util/FP16.java
*/
public class Float16 {
// Positive infinity of type half-precision float.
public static final short POSITIVE_INFINITY = (short) 0x7c00;
// A Not-a-Number representation of a half-precision float.
public static final short NaN = (short) 0x7e00;
// The bitmask to and a number with to obtain the sign bit.
private static final int SIGN_MASK = 0x8000;
// The offset to shift by to obtain the exponent bits.
private static final int EXPONENT_SHIFT = 10;
// The bitmask to and a number shifted by EXPONENT_SHIFT right, to obtain exponent bits.
private static final int SHIFTED_EXPONENT_MASK = 0x1f;
// The bitmask to and a number with to obtain significand bits.
private static final int SIGNIFICAND_MASK = 0x3ff;
// The offset of the exponent from the actual value.
private static final int EXPONENT_BIAS = 15;
// The offset to shift by to obtain the sign bit.
private static final int SIGN_SHIFT = 15;
// The bitmask to AND with to obtain exponent and significand bits.
private static final int EXPONENT_SIGNIFICAND_MASK = 0x7fff;
private static final int FP32_SIGN_SHIFT = 31;
private static final int FP32_EXPONENT_SHIFT = 23;
private static final int FP32_SHIFTED_EXPONENT_MASK = 0xff;
private static final int FP32_SIGNIFICAND_MASK = 0x7fffff;
private static final int FP32_EXPONENT_BIAS = 127;
private static final int FP32_QNAN_MASK = 0x400000;
private static final int FP32_DENORMAL_MAGIC = 126 << 23;
private static final float FP32_DENORMAL_FLOAT = Float.intBitsToFloat(FP32_DENORMAL_MAGIC);
/**
* Returns true if the specified half-precision float value represents a Not-a-Number, false
* otherwise.
*
* @param h A half-precision float value
* @return True if the value is a NaN, false otherwise
*/
@VisibleForTesting
public static boolean isNaN(short h) {
return (h & EXPONENT_SIGNIFICAND_MASK) > POSITIVE_INFINITY;
}
/**
* Compares the two specified half-precision float values. The following conditions apply during
* the comparison:
*
* <ul>
* <li>NaN is considered by this method to be equal to itself and greater than all other
* half-precision float values (including {@code #POSITIVE_INFINITY})
* <li>POSITIVE_ZERO is considered by this method to be greater than NEGATIVE_ZERO.
* </ul>
*
* @param x The first half-precision float value to compare.
* @param y The second half-precision float value to compare
* @return The value {@code 0} if {@code x} is numerically equal to {@code y}, a value less than
* {@code 0} if {@code x} is numerically less than {@code y}, and a value greater than {@code
* 0} if {@code x} is numerically greater than {@code y}
*/
@VisibleForTesting
public static int compare(short x, short y) {
boolean xIsNaN = isNaN(x);
boolean yIsNaN = isNaN(y);
if (!xIsNaN && !yIsNaN) {
int first = ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff);
int second = ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff);
// Returns true if the first half-precision float value is less
// (smaller toward negative infinity) than the second half-precision float value.
if (first < second) {
return -1;
}
// Returns true if the first half-precision float value is greater
// (larger toward positive infinity) than the second half-precision float value.
if (first > second) {
return 1;
}
}
// Collapse NaNs, akin to halfToIntBits(), but we want to keep
// (signed) short value types to preserve the ordering of -0.0
// and +0.0
short xBits = xIsNaN ? NaN : x;
short yBits = yIsNaN ? NaN : y;
return (xBits == yBits ? 0 : (xBits < yBits ? -1 : 1));
}
/**
* Converts the specified half-precision float value into a single-precision float value. The
* following special cases are handled: If the input is NaN, the returned value is Float NaN. If
* the input is POSITIVE_INFINITY or NEGATIVE_INFINITY, the returned value is respectively Float
* POSITIVE_INFINITY or Float NEGATIVE_INFINITY. If the input is 0 (positive or negative), the
* returned value is +/-0.0f. Otherwise, the returned value is a normalized single-precision float
* value.
*
* @param b The half-precision float value to convert to single-precision
* @return A normalized single-precision float value
*/
@VisibleForTesting
public static float toFloat(short b) {
int bits = b & 0xffff;
int s = bits & SIGN_MASK;
int e = (bits >>> EXPONENT_SHIFT) & SHIFTED_EXPONENT_MASK;
int m = bits & SIGNIFICAND_MASK;
int outE = 0;
int outM = 0;
if (e == 0) { // Denormal or 0
if (m != 0) {
// Convert denorm fp16 into normalized fp32
float o = Float.intBitsToFloat(FP32_DENORMAL_MAGIC + m);
o -= FP32_DENORMAL_FLOAT;
return s == 0 ? o : -o;
}
} else {
outM = m << 13;
if (e == 0x1f) { // Infinite or NaN
outE = 0xff;
if (outM != 0) { // SNaNs are quieted
outM |= FP32_QNAN_MASK;
}
} else {
outE = e - EXPONENT_BIAS + FP32_EXPONENT_BIAS;
}
}
int out = (s << 16) | (outE << FP32_EXPONENT_SHIFT) | outM;
return Float.intBitsToFloat(out);
}
/**
* Converts the specified single-precision float value into a half-precision float value. The
* following special cases are handled:
*
* <p>If the input is NaN, the returned value is NaN. If the input is Float POSITIVE_INFINITY or
* Float NEGATIVE_INFINITY, the returned value is respectively POSITIVE_INFINITY or
* NEGATIVE_INFINITY. If the input is 0 (positive or negative), the returned value is
* POSITIVE_ZERO or NEGATIVE_ZERO. If the input is a less than MIN_VALUE, the returned value is
* flushed to POSITIVE_ZERO or NEGATIVE_ZERO. If the input is a less than MIN_NORMAL, the returned
* value is a denorm half-precision float. Otherwise, the returned value is rounded to the nearest
* representable half-precision float value.
*
* @param f The single-precision float value to convert to half-precision
* @return A half-precision float value
*/
public static short toFloat16(float f) {
int bits = Float.floatToRawIntBits(f);
int s = (bits >>> FP32_SIGN_SHIFT);
int e = (bits >>> FP32_EXPONENT_SHIFT) & FP32_SHIFTED_EXPONENT_MASK;
int m = bits & FP32_SIGNIFICAND_MASK;
int outE = 0;
int outM = 0;
if (e == 0xff) { // Infinite or NaN
outE = 0x1f;
outM = m != 0 ? 0x200 : 0;
} else {
e = e - FP32_EXPONENT_BIAS + EXPONENT_BIAS;
if (e >= 0x1f) { // Overflow
outE = 0x1f;
} else if (e <= 0) { // Underflow
if (e < -10) {
// The absolute fp32 value is less than MIN_VALUE, flush to +/-0
} else {
// The fp32 value is a normalized float less than MIN_NORMAL,
// we convert to a denorm fp16
m = m | 0x800000;
int shift = 14 - e;
outM = m >> shift;
int lowm = m & ((1 << shift) - 1);
int hway = 1 << (shift - 1);
// if above halfway or exactly halfway and outM is odd
if (lowm + (outM & 1) > hway) {
// Round to nearest even
// Can overflow into exponent bit, which surprisingly is OK.
// This increment relies on the +outM in the return statement below
outM++;
}
}
} else {
outE = e;
outM = m >> 13;
// if above halfway or exactly halfway and outM is odd
if ((m & 0x1fff) + (outM & 0x1) > 0x1000) {
// Round to nearest even
// Can overflow into exponent bit, which surprisingly is OK.
// This increment relies on the +outM in the return statement below
outM++;
}
}
}
// The outM is added here as the +1 increments for outM above can
// cause an overflow in the exponent bit which is OK.
return (short) ((s << SIGN_SHIFT) | (outE << EXPONENT_SHIFT) + outM);
}
/**
* Returns a string representation of the specified half-precision float value. Calling this
* method is equivalent to calling <code>Float.toString(toFloat(h))</code>. See {@link
* Float#toString(float)} for more information on the format of the string representation.
*
* @param h A half-precision float value in binary little-endian format
* @return A string representation of the specified value
*/
@VisibleForTesting
public static String toFloatString(short h) {
return Float.toString(Float16.toFloat(h));
}
}