Java 中的半精度浮点

2022-09-01 05:51:49

是否有任何 Java 库可以对 IEEE 754 半精度数字执行计算或将它们与双精度数字相互转换?

以下任一方法都适用:

  • 将数字保持为半精度格式,并使用整数算术和位微调进行计算(如MicroFloat对单精度和双精度所做的那样)
  • 以单精度或双精度执行所有计算,转换为/从半精度进行传输(在这种情况下,我需要的是经过充分测试的转换函数。

编辑:转换需要100%准确 - 输入文件中有很多NaN,无穷大和次正规。


相关问题,但对于JavaScript:在Javascript中解压缩半精度浮点数


答案 1

您可以使用和转换它们与原始浮点值相互转换。如果您可以忍受截断的精度(而不是舍入),则只需几个位移位即可实现转换。Float.intBitsToFloat()Float.floatToIntBits()

我现在已经投入了更多的精力,结果并不像我一开始想象的那么简单。这个版本现在在我所能想象到的方方面面都经过了测试和验证,我非常有信心它能为所有可能的输入值产生确切的结果。它支持任一方向上的精确舍入和非正态转换。

// ignores the higher 16 bits
public static float toFloat( int hbits )
{
    int mant = hbits & 0x03ff;            // 10 bits mantissa
    int exp =  hbits & 0x7c00;            // 5 bits exponent
    if( exp == 0x7c00 )                   // NaN/Inf
        exp = 0x3fc00;                    // -> NaN/Inf
    else if( exp != 0 )                   // normalized value
    {
        exp += 0x1c000;                   // exp - 15 + 127
        if( mant == 0 && exp > 0x1c400 )  // smooth transition
            return Float.intBitsToFloat( ( hbits & 0x8000 ) << 16
                                            | exp << 13 | 0x3ff );
    }
    else if( mant != 0 )                  // && exp==0 -> subnormal
    {
        exp = 0x1c400;                    // make it normal
        do {
            mant <<= 1;                   // mantissa * 2
            exp -= 0x400;                 // decrease exp by 1
        } while( ( mant & 0x400 ) == 0 ); // while not normal
        mant &= 0x3ff;                    // discard subnormal bit
    }                                     // else +/-0 -> +/-0
    return Float.intBitsToFloat(          // combine all parts
        ( hbits & 0x8000 ) << 16          // sign  << ( 31 - 15 )
        | ( exp | mant ) << 13 );         // value << ( 23 - 10 )
}

// returns all higher 16 bits as 0 for all results
public static int fromFloat( float fval )
{
    int fbits = Float.floatToIntBits( fval );
    int sign = fbits >>> 16 & 0x8000;          // sign only
    int val = ( fbits & 0x7fffffff ) + 0x1000; // rounded value

    if( val >= 0x47800000 )               // might be or become NaN/Inf
    {                                     // avoid Inf due to rounding
        if( ( fbits & 0x7fffffff ) >= 0x47800000 )
        {                                 // is or must become NaN/Inf
            if( val < 0x7f800000 )        // was value but too large
                return sign | 0x7c00;     // make it +/-Inf
            return sign | 0x7c00 |        // remains +/-Inf or NaN
                ( fbits & 0x007fffff ) >>> 13; // keep NaN (and Inf) bits
        }
        return sign | 0x7bff;             // unrounded not quite Inf
    }
    if( val >= 0x38800000 )               // remains normalized value
        return sign | val - 0x38000000 >>> 13; // exp - 127 + 15
    if( val < 0x33000000 )                // too small for subnormal
        return sign;                      // becomes +/-0
    val = ( fbits & 0x7fffffff ) >>> 23;  // tmp exp for subnormal calc
    return sign | ( ( fbits & 0x7fffff | 0x800000 ) // add subnormal bit
         + ( 0x800000 >>> val - 102 )     // round depending on cut off
      >>> 126 - val );   // div by 2^(1-(exp-127+15)) and >> 13 | exp=0
}

本书相比,我实现了两个小扩展,因为16位浮点数的一般精度相当低,这使得浮点格式的固有异常与较大的浮点类型相比,在视觉上可察觉,因为较大的浮点类型由于精度充足而通常不会被注意到。

第一个是函数中的这两行:toFloat()

if( mant == 0 && exp > 0x1c400 )  // smooth transition
    return Float.intBitsToFloat( ( hbits & 0x8000 ) << 16 | exp << 13 | 0x3ff );

类型大小正常范围内的浮点数采用指数,从而将精度精确到值的大小。但这不是一个顺利的采用,它分步进行:切换到下一个更高的指数会导致一半的精度。现在,尾数的所有值的精度保持不变,直到下一次跳转到下一个更高的指数。上面的扩展代码通过返回一个值,该值位于此特定半浮点值的已覆盖 32 位浮点值范围的地理中心,使这些转换更加平滑。每个正常的半浮点值正好映射到 8192 个 32 位浮点值。返回的值应该正好位于这些值的中间。但是在半浮点指数的转换处,较低的 4096 值的精度是较高的 4096 值的两倍,因此覆盖的数字空间仅为另一侧的一半。所有这些 8192 32 位浮点值都映射到相同的半浮点值,因此无论选择 8192 中间 32 位值中的哪一个,将半浮点数转换为 32 位并返回都会产生相同的半浮点值。现在,扩展在过渡时会产生一个更平滑的半步,在过渡时乘以 sqrt(2) 的系数,如下图所示,而左应该将锐化步长可视化 2 倍,而不会出现抗锯齿。您可以安全地从代码中删除这两行,以获得标准行为。

covered number space on either side of the returned value:
       6.0E-8             #######                  ##########
       4.5E-8             |                       #
       3.0E-8     #########               ########

第二个扩展在函数中:fromFloat()

    {                                     // avoid Inf due to rounding
        if( ( fbits & 0x7fffffff ) >= 0x47800000 )
...
        return sign | 0x7bff;             // unrounded not quite Inf
    }

此扩展通过保存一些 32 位值形式以提升为无穷大,略微扩展了半浮点数格式的数字范围。受影响的值是那些在没有舍入的情况下会小于无穷大的值,并且仅由于舍入而变为无穷大的值。如果您不需要此扩展名,可以安全地删除上面显示的行。

我试图尽可能地优化函数中正常值的路径,由于使用了预先计算和未移动的常量,这使得它的可读性降低。我没有在“toFloat()”上投入太多精力,因为它无论如何都不会超过查找表的性能。因此,如果速度真的很重要,则可以仅使用该函数来填充具有0x10000元素的静态查找表,而不是将此表用于实际转换。使用当前的 x64 服务器 VM,这大约快 3 倍,使用 x86 客户端 VM 时,速度大约快 5 倍。fromFloat()toFloat()

我特此将代码置于公共领域。


答案 2

x4u 的代码将值 1 正确编码为0x3c00(ref:https://en.wikipedia.org/wiki/Half-precision_floating-point_format)。但是具有平滑度改进的解码器将其解码为1.000122。维基百科条目说整数值0..2048可以精确表示。不好...
从 toFloat 代码中删除 可确保对于 -2048..2048 范围内的整数 k,可能以降低平滑度为代价。"| 0x3ff"toFloat(fromFloat(k)) == k