我一直在努力阅读一个用Java程序编写的二进制文件(我正在将Java库移植到C#,并希望与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中它在转换为long类型之前就是确切的7072
,在这种情况下它仍然是7072
。为了使因子确切的为7072
,浮点值必须是3.492345679012346
。
可以安全地假设浮点数的值实际上是3.492345679012346
而不是3.4923456
(在Eclipse中显示的值)吗?
.NET等效性
现在,我正在寻找在.NET中获得完全相同结果的方法。但到目前为止,我只能使用一种黑客方式读取这个文件,并且我并不完全确定这种黑客方法是否适用于Java库生成的任何文件。
根据Java中的intBitsToFloat方法与C#中的等效功能?,相应的功能是使用:
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
,这比Java给出的值7072
要小。
尝试过的方法
于是我假设在Float.intBitsToFloat(someInt)
和BitConverter.ToSingle(BitConverter.GetBytes(value), 0)
之间存在某些数学差异,以获得如此不同的结果。因此,我查阅了intBitsToFloat(int)的javadocs,看看能否在.NET中重现Java的结果。我最终得到了:
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
之前,该数字比所需的Java值(3.492345679012346
) 要低得多,后者需要结果正好为7072
。
我尝试了这个解决方法,但结果值完全相同,为3.49234557
。
我还尝试过四舍五入和截断,但当然会使其他接近整数的值都错误。
当浮点值在某个特定范围内接近整数时,通过更改计算方式,我能够解决这个问题。但由于可能存在其他计算非常接近整数的地方,因此这种解决方案可能不适用于所有情况。
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中编写一个函数,返回与Java中的 Float.intBitsToFloat(int)
完全相同 的结果?或者,如何规范化浮点计算中的差异,使得给定值 1080001175
和 2025
时,结果是 7072
而不是 7071
?
注意:对于所有其他可能的整数值,它应该与Java的表现一致。上面的情况只是其中可能有许多计算方式不同的情况之一。
我正在使用.NET Framework 4.5.1和.NET Standard 1.5,并且应该在
x86
和x64
环境下产生相同的结果。
intBitsToFloat
是一个本地方法,它调用C中的Java_java_lang_Float_intBitsToFloat
方法,请参见Float.c内容。它将jint
转换为long
并返回jfloat
,而jfloat
本身可能与System.Single
使用不同的舍入机制。更奇怪的是,在使用与Java实现相同的代码进行转换为long
时,C#中的结果float
会向下舍入。 - Tetsuya Yamamoto(long)BigInteger.Divide(BigInteger.Multiply(new BigInteger(idx), new BigInteger(10000000 * averages[block])), new BigInteger(10000000))
成功地解决了这个问题。如果有一种方法可以在.NET中调用本机C函数,那么这可能会大大解决这个问题。 - NightOwl888long result2 = (long)(float)(idx * result);
它会在生成的IL中添加一个conv.r4
操作码,从而在计算堆栈中某个地方强制进行浮点数实现(即使Visual Studio将其报告为“不必要的转换”...)。我想这是jit优化。也许是一个bug?如果你编译成.NET 2.0,它就不会发生... - Simon MourierFloat.c
中呈现的union
在C#中有类似的实现,使用StructLayout(LayoutKind.Explicit)
和FieldOffset(n)
属性来声明struct
(适用于简单类型,请参见https://dev59.com/YXVC5IYBdhLWcg3w-mZO)。有趣的是,在`jni.h`中,`jfloat`本身被定义为`typedef float jfloat,这意味着
jfloat本身只是C类型
float`的别名。 - Tetsuya Yamamoto(long)(float)(idx * result);
,它似乎解决了这个问题。我需要进行更多的测试来验证它在 .NET 4.5.1 和 .NET Standard 1.5 上是否适用于 x86 和 x64,但这看起来很有前途。请将您的评论作为答案添加,以便我可以接受此解决方案。 - NightOwl888