Как мне Mimic Number.intBitsToFloat() в С#?

Я с ума сходил, пытаясь прочитать двоичный файл, написанный с использованием Java-программы (я переношу библиотеку Java на С# и хочу поддерживать совместимость с версией Java).

Библиотека Java

Автор компонента решил использовать float вместе с умножением, чтобы определить смещения начала и конца части данных. К сожалению, есть различия в том, как он работает в .NET, чем с Java. В Java библиотека использует Float.intBitsToFloat(someInt), где значение someInt равно 1080001175.

int someInt = 1080001175;
float result = Float.intBitsToFloat(someInt);
// result (as viewed in Eclipse): 3.4923456

Позже это число умножается на значение, определяющее начальную и конечную позиции. В этом случае проблема возникает, когда значение индекса 2025.

int idx = 2025;
long result2 = (long)(idx * result);
// result2: 7072

Согласно моему калькулятору, результатом этого расчета должно быть 7071.99984. Но в Java это точно 7072, прежде чем он будет отброшен до длинного, и в этом случае он все еще 7072. Для того чтобы коэффициент был точно 7072, значение float должно было бы быть 3.492345679012346.

Можно ли предположить, что значение float фактически 3.492345679012346 вместо 3.4923456 (значение, показанное в Eclipse)?

Эквивалент .NET

Теперь я ищу способ получить тот же результат в .NET. Но до сих пор я мог прочитать этот файл только с помощью взлома, и я не совсем уверен, что хак будет работать для любого файла, который создается библиотекой на Java.

В соответствии с методом intBitsToFloat в Java VS С#? эквивалентная функциональность использует:

int someInt = 1080001175;
int result = BitConverter.ToSingle(BitConverter.GetBytes(someInt), 0);
// result: 3.49234557

Это делает расчет:

int idx = 2025;
long result2 = (long)(idx * result);
// result2: 7071

Результат до длинного каста 7071.99977925, который лишен значения 7072, которое дает Java.

Что я пробовал

Оттуда я предположил, что в математике между Float.intBitsToFloat(someInt) и BitConverter.ToSingle(BitConverter.GetBytes(value), 0) должна быть какая-то разница, чтобы получать такие разные результаты. Итак, я посоветовался с javadocs для intBitsToFloat (int), чтобы увидеть, могу ли я воспроизвести результаты Java в .NET. Я закончил с:

public static float Int32BitsToSingle(int value)
{
    if (value == 0x7f800000)
    {
        return float.PositiveInfinity;
    }
    else if ((uint)value == 0xff800000)
    {
        return float.NegativeInfinity;
    }
    else if ((value >= 0x7f800001 && value <= 0x7fffffff) || ((uint)value >= 0xff800001 && (uint)value <= 0xffffffff))
    {
        return float.NaN;
    }

    int bits = value;
    int s = ((bits >> 31) == 0) ? 1 : -1;
    int e = ((bits >> 23) & 0xff);
    int m = (e == 0) ? (bits & 0x7fffff) >> 1 : (bits & 0x7fffff) | 0x800000;

    //double r = (s * m * Math.Pow(2, e - 150));
    // value of r: 3.4923455715179443

    float result = (float)(s * m * Math.Pow(2, e - 150));
    // value of result: 3.49234557

    return result;
}

Как вы можете видеть, результат точно такой же, как при использовании BitConverter, и перед тем, как придать значение float, число будет немного ниже (3.4923455715179443), чем предполагаемое значение Java (3.492345679012346), который необходим для того, чтобы результат был точно 7072.

Я попробовал это решение, но результирующее значение точно такое же, 3.49234557.

Я также пытался округлить и обрезать, но, конечно, это делает все другие значения, которые не очень близки к целому числу, неверны.

Мне удалось взломать это, изменив вычисление, когда значение float находится в пределах определенного диапазона целого числа, но поскольку могут быть другие места, где расчет очень близок к целому числу, это решение, вероятно, выиграло Работает повсеместно.

float avg = (idx * averages[block]);
avgValue = (long)avg; // yields 7071
if ((avgValue + 1) - avg < 0.0001)
{
    avgValue = Convert.ToInt64(avg); // yields 7072
}

Обратите внимание, что функция Convert.ToInt64 также не работает в большинстве случаев, но она имеет эффект округления в этом конкретном случае.

Вопрос

Как я могу сделать функцию в .NET, которая возвращает точно такой же результат, что и Float.intBitsToFloat(int) в Java? Или, как я могу иначе нормализовать различия в вычислении float, чтобы этот результат был 7072 (not 7071) с учетом значений 1080001175 и 2025?

Примечание. Он должен работать так же, как и Java для всех других возможных значений целого числа. Вышеприведенный случай является лишь одним из потенциально многих мест, где вычисления отличаются в .NET.

Я использую .NET Framework 4.5.1 и .NET Standard 1.5, и он должен давать те же результаты в средах x86 и x64.

Ответы

Ответ 1

Определение 4-байтового числа с плавающей запятой в С# и Java (и любой другой достойной платформе программирования) основано на стандартах IEEE, поэтому двоичный формат тот же.

Итак, он должен работать. И на самом деле это работает, но только для целей X64 (мои более ранние комментарии о .NET 2 и 4 могут быть неправильными или правильными, я не могу проверить старые двоичные файлы старой платформы).

Если вы хотите, чтобы он работал для всех целей, вам необходимо определить его следующим образом:

long result2 = (long)(float)(idx * result);

Если вы посмотрите на сгенерированный ИЛ, он добавит дополнительный код conv.r4 после умножения. Я предполагаю, что это принудительно реализует число с плавающей точкой в ​​скомпилированном коде x86. Я полагаю, что это проблема оптимизации jit.

Я не знаю достаточно о оптимизации jit, чтобы определить, является ли это ошибкой или нет. Самое смешное, что среда IDE Visual Studio 2017 даже выделяет текст (float) и сообщает, что он отличен как "избыточный" или "лишний", поэтому он не пахнет хорошо.