Как мне 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)
и сообщает, что он отличен как "избыточный" или "лишний", поэтому он не пахнет хорошо.