Подтвердить что ты не робот

Плавная точка с плавающей запятой в Java

Есть ли библиотека Java где угодно, которая может выполнять вычисления на IEEE 754 half-precision или конвертировать их в двойную точность и с двойной точностью?

Любой из этих подходов был бы подходящим:

  • Сохраняйте цифры в формате полуточности и вычисляйте с использованием целочисленной арифметики и бит-twiddling (как MicroFloat для одно- и с двойной точностью)
  • Выполнять все вычисления в одиночной или двойной точности, конвертируя в/из половинной точности для передачи (в этом случае мне нужны хорошо проверенные функции преобразования.)

Изменить: преобразование должно быть на 100% точным - в входных файлах есть много NaN, бесконечности и субнормальные значения.


Связанный с этим вопрос, но для JavaScript: Декомпрессия полуточности в Javascript

4b9b3361

Ответ 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-битовые значения с плавающей точкой соответствуют одному и тому же значению с половинной плавающей точкой, поэтому преобразование полуполя в 32-битное и обратно приводит к тому же полуполяному значению независимо от того, какой из 3219-разрядных 32-разрядных значений был выбран. Расширение теперь приводит к чему-то наподобие более гладкого полушага на коэффициент sqrt (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-битных значений, получая повышение до бесконечности. Затрагиваемые значения - это те, которые были бы меньше, чем бесконечность без округления, и стали бы бесконечными только благодаря округлению. Вы можете безопасно удалить строки, показанные выше, если вы не хотите этого расширения.

Я попытался максимально оптимизировать путь для нормальных значений в функции fromFloat(), что сделало его менее понятным из-за использования предварительно вычисленных и непересекающихся констант. Я не приложил столько усилий к 'toFloat()', так как он все равно не превысит производительность таблицы поиска. Поэтому, если скорость действительно имеет значение, можно использовать функцию toFloat() только для заполнения статической таблицы поиска с помощью элементов 0x10000, а не для использования этой таблицы для фактического преобразования. Это примерно в 3 раза быстрее с текущей VM-сервером x64 и примерно в 5 раз быстрее с клиентской виртуальной машиной x86.

Я помещаю код в это общедоступное.

Ответ 2

Код x4u правильно кодирует значение 1 как 0x3c00 (ref: https://en.wikipedia.org/wiki/Half-precision_floating-point_format). Но декодер с улучшением гладкости декодирует это в 1.000122. В записи wikipedia указано, что целые значения 0..2048 могут быть представлены точно. Не приятно...
Удаление "| 0x3ff" из кода toFloat гарантирует, что toFloat(fromFloat(k)) == k для целого числа k в диапазоне -2048..2048, вероятно, за счет немного меньшей гладкости.

Ответ 3

Прежде чем я увидел решение, размещенное здесь, я взломал что-то простое:

public static float toFloat(int nHalf)
    {
    int S = (nHalf >>> 15) & 0x1;                                                             
    int E = (nHalf >>> 10) & 0x1F;                                                            
    int T = (nHalf       ) & 0x3FF;                                                           

    E = E == 0x1F                                                                            
            ? 0xFF  // it 2^w-1; it all 1's, so keep it all 1 for the 32-bit float       
            : E - 15 + 127;     // adjust the exponent from the 16-bit bias to the 32-bit bias

    // sign S is now bit 31                                                                    
    // exp E is from bit 30 to bit 23                                                          
    // scale T by 13 binary digits (it grew from 10 to 23 bits)                                
    return Float.intBitsToFloat(S << 31 | E << 23 | T << 13);                               
    }

Мне нравится подход в другом опубликованном решении. Для справки:

    // notes from the IEEE-754 specification:

    // left to right bits of a binary floating point number:
    // size        bit ids       name  description
    // ----------  ------------  ----  ---------------------------
    // 1 bit                       S   sign
    // w bits      E[0]..E[w-1]    E   biased exponent
    // t=p-1 bits  d[1]..d[p-1]    T   trailing significant field

    // The range of the encoding’s biased exponent E shall include:
    // ― every integer between 1 and 2^w − 2, inclusive, to encode normal numbers
    // ― the reserved value 0 to encode ±0 and subnormal numbers
    // ― the reserved value 2w − 1 to encode +/-infinity and NaN

    // The representation r of the floating-point datum, and value v of the floating-point datum
    // represented, are inferred from the constituent fields as follows:
    // a) If E == 2^w−1 and T != 0, then r is qNaN or sNaN and v is NaN regardless of S
    // b) If E == 2^w−1 and T == 0, then r=v=(−1)^S * (+infinity)
    // c) If 1 <= E <= 2^w−2, then r is (S, (E−bias), (1 + 2^(1−p) * T))
    //    the value of the corresponding floating-point number is
    //        v = (−1)^S * 2^(E−bias) * (1 + 2^(1−p) * T)
    //    thus normal numbers have an implicit leading significand bit of 1
    // d) If E == 0 and T != 0, then r is (S, emin, (0 + 2^(1−p) * T))
    //    the value of the corresponding floating-point number is
    //        v = (−1)^S * 2^emin * (0 + 2^(1−p) * T)
    //    thus subnormal numbers have an implicit leading significand bit of 0
    // e) If E == 0 and T ==0, then r is (S, emin, 0) and v = (−1)^S * (+0)

    // parameter                                      bin16  bin32
    // --------------------------------------------   -----  -----
    // k, storage width in bits                         16     32
    // p, precision in bits                             11     24
    // emax, maxiumum exponent e                        15    127
    // bias, E-e                                        15    127
    // sign bit                                          1      1
    // w, exponent field width in bits                   5      8
    // t, trailing significant field width in bits      10     23