为什么 BitConverter 在转换浮点数和字节时看似 return 不正确的结果?
Why does BitConverter seemingly return incorrect results when converting floats and bytes?
我正在使用 C# 并尝试将四个字节打包成一个浮点数(上下文是游戏开发,其中 RGBA 颜色被打包成一个值)。为此,我使用了 BitConverter
,但某些转换似乎会导致不正确的字节。以下面的例子(使用字节0, 0, 129, 255
):
var before = new [] { (byte)0, (byte)0, (byte)129, (byte)255 };
var f = BitConverter.ToSingle(before, 0); // Results in NaN
var after = BitConverter.GetBytes(f); // Results in bytes 0, 0, 193, 255
使用https://www.h-schmidt.net/FloatConverter/IEEE754.html,我验证了我开始的四个字节(0, 0, 129, 255
,相当于二进制00000000000000001000000111111111
)表示浮点值4.66338115943e-41
。通过翻转字节顺序(二进制 11111111100000010000000000000000
),我得到 NaN
(与上面代码中的 f
匹配)。但是当我将该浮点数转换回字节时,我得到 0, 0, 193, 255
(注意 193
当我期待 129
时)。
奇怪的是,运行 这个相同的字节 0, 0, 128, 255
示例是正确的(浮点值 f
变为 -Infinity
,然后转换回字节产生 0, 0, 128, 255
)。鉴于这个事实,我怀疑 NaN
是相关的。
任何人都可以阐明这里发生的事情吗?
更新: 问题 Converting 2 bytes to Short in C# 被列为重复问题,但这是不准确的。该问题试图将字节转换为一个值(在这种情况下,将两个字节转换为一个短字节)并且不正确的字节序给出了一个意外的值。在我的例子中,实际的浮点值是无关紧要的(因为我没有使用转换后的值作为浮点数)。相反,我试图通过首先转换为浮点数,然后再转换回来,有效地将四个字节重新解释为浮点数 直接。如图所示,来回有时 returns 与我发送的字节不同。
第二次更新:我简单说一下我的问题。正如 Peter Duniho 评论的那样,BitConverter
永远不会 修改 您传入的字节,而只是将它们复制到新的内存位置并重新解释结果。但是,正如我的示例所示,可以发送四个字节 (0, 0, 129, 255
),这些字节在内部被复制并重新解释为浮点数,然后将该浮点数转换回 不同的字节 比原件 (0, 0, 193, 255
).
与BitConverter
相关的字节顺序经常被提及。但是,在这种情况下,我觉得字节序不是根本问题。当我调用 BitConverter.ToSingle
时,我传入了一个包含四个字节的数组。这些字节代表一些转换为浮点数的二进制(32 位)。通过在函数调用之前更改字节顺序,我所做的只是更改我发送到函数中的位。不管那些位的 value,应该可以将它们转换为浮点数(也是 32 位),然后将浮点数转换回得到 相同的位我已发送。如我的示例所示,使用字节 0, 0, 129, 255
(二进制 00000000000000001000000111111111
)会产生浮点值。我想获取该值(由这些位表示的浮点数)并将其转换为原始的四个字节。
这在所有情况下都可以在 C# 中实现吗?
经过研究、实验和与朋友讨论,此行为的根本原因(转换为浮点数时字节发生变化)似乎是signaling vs. quiet NaNs (as Hans Passant also pointed out in a comment). I'm no expert on signaling and quiet NaNs, but from what I understand, quiet NaNs have the highest-order bit of the mantissa set to one, while signaling NaNs have that bit set to zero. See the following image (taken from https://www.h-schmidt.net/FloatConverter/IEEE754.html)供参考。我在每组八位周围绘制了四个彩色框,以及一个指向最高阶尾数位的箭头。
当然,我发布的问题不是关于浮点位布局或信号与安静 NaN 的对比,而是简单地询问为什么我的编码字节似乎被修改了。答案是 C# 运行 时间(或者至少我 假设 它是 C# 运行 时间)在内部 将所有信号 NaN 转换为安静,这意味着 在该位置编码的字节将其第二位从零交换为 1。
例如,字节 0, 0, 129, 255
(以相反的顺序编码,我认为是由于字节序)将值 129
放在第二个字节(绿色框)中。 129
在二进制中是 10000001
,因此翻转它的第二位得到 11000001
,即 193
(正是我在原始示例中看到的)。这种相同的模式(编码字节的值已更改)适用于 129-191
范围内的所有字节(含)。字节 128
和更低的字节不是 NaN,而字节 192
和更高的 是 NaN,但没有修改它们的值,因为它们的第二位(位于最高位尾数位)已经是一个。
这样就回答了为什么会出现这种行为,但在我看来,还有两个问题:
- 是否可以在 C# 中禁用此行为(将信号 NaN 转换为安静)?
- 如果不是,解决方法是什么?
第一个问题的答案似乎是没有(如果我了解到其他情况,我会修改这个答案)。但是,请务必注意,此行为在所有 .NET 版本中并不一致。在我的计算机上,NaN 在我尝试的每个 .NET Framework 版本(从 4.8.0 开始,然后向下工作)上都被转换(即我的编码字节发生了变化)。 .NET Core 3 和 .NET 5(我没有测试每个可用版本)。此外,一位朋友能够在 .NET Framework 4.7.2 上 运行 相同的示例代码,令人惊讶的是,字节在他的机器上 未 修改。不同 C# 的内部结构 运行 次不是我的专业领域,但足以说明版本和计算机之间存在差异。
第二个问题的答案是,正如其他人所建议的那样,完全避免浮点数转换。相反,每组四个字节(在我的例子中代表 RGBA 颜色)可以用整数编码或直接添加到字节数组。
我正在使用 C# 并尝试将四个字节打包成一个浮点数(上下文是游戏开发,其中 RGBA 颜色被打包成一个值)。为此,我使用了 BitConverter
,但某些转换似乎会导致不正确的字节。以下面的例子(使用字节0, 0, 129, 255
):
var before = new [] { (byte)0, (byte)0, (byte)129, (byte)255 };
var f = BitConverter.ToSingle(before, 0); // Results in NaN
var after = BitConverter.GetBytes(f); // Results in bytes 0, 0, 193, 255
使用https://www.h-schmidt.net/FloatConverter/IEEE754.html,我验证了我开始的四个字节(0, 0, 129, 255
,相当于二进制00000000000000001000000111111111
)表示浮点值4.66338115943e-41
。通过翻转字节顺序(二进制 11111111100000010000000000000000
),我得到 NaN
(与上面代码中的 f
匹配)。但是当我将该浮点数转换回字节时,我得到 0, 0, 193, 255
(注意 193
当我期待 129
时)。
奇怪的是,运行 这个相同的字节 0, 0, 128, 255
示例是正确的(浮点值 f
变为 -Infinity
,然后转换回字节产生 0, 0, 128, 255
)。鉴于这个事实,我怀疑 NaN
是相关的。
任何人都可以阐明这里发生的事情吗?
更新: 问题 Converting 2 bytes to Short in C# 被列为重复问题,但这是不准确的。该问题试图将字节转换为一个值(在这种情况下,将两个字节转换为一个短字节)并且不正确的字节序给出了一个意外的值。在我的例子中,实际的浮点值是无关紧要的(因为我没有使用转换后的值作为浮点数)。相反,我试图通过首先转换为浮点数,然后再转换回来,有效地将四个字节重新解释为浮点数 直接。如图所示,来回有时 returns 与我发送的字节不同。
第二次更新:我简单说一下我的问题。正如 Peter Duniho 评论的那样,BitConverter
永远不会 修改 您传入的字节,而只是将它们复制到新的内存位置并重新解释结果。但是,正如我的示例所示,可以发送四个字节 (0, 0, 129, 255
),这些字节在内部被复制并重新解释为浮点数,然后将该浮点数转换回 不同的字节 比原件 (0, 0, 193, 255
).
与BitConverter
相关的字节顺序经常被提及。但是,在这种情况下,我觉得字节序不是根本问题。当我调用 BitConverter.ToSingle
时,我传入了一个包含四个字节的数组。这些字节代表一些转换为浮点数的二进制(32 位)。通过在函数调用之前更改字节顺序,我所做的只是更改我发送到函数中的位。不管那些位的 value,应该可以将它们转换为浮点数(也是 32 位),然后将浮点数转换回得到 相同的位我已发送。如我的示例所示,使用字节 0, 0, 129, 255
(二进制 00000000000000001000000111111111
)会产生浮点值。我想获取该值(由这些位表示的浮点数)并将其转换为原始的四个字节。
这在所有情况下都可以在 C# 中实现吗?
经过研究、实验和与朋友讨论,此行为的根本原因(转换为浮点数时字节发生变化)似乎是signaling vs. quiet NaNs (as Hans Passant also pointed out in a comment). I'm no expert on signaling and quiet NaNs, but from what I understand, quiet NaNs have the highest-order bit of the mantissa set to one, while signaling NaNs have that bit set to zero. See the following image (taken from https://www.h-schmidt.net/FloatConverter/IEEE754.html)供参考。我在每组八位周围绘制了四个彩色框,以及一个指向最高阶尾数位的箭头。
当然,我发布的问题不是关于浮点位布局或信号与安静 NaN 的对比,而是简单地询问为什么我的编码字节似乎被修改了。答案是 C# 运行 时间(或者至少我 假设 它是 C# 运行 时间)在内部 将所有信号 NaN 转换为安静,这意味着 在该位置编码的字节将其第二位从零交换为 1。
例如,字节 0, 0, 129, 255
(以相反的顺序编码,我认为是由于字节序)将值 129
放在第二个字节(绿色框)中。 129
在二进制中是 10000001
,因此翻转它的第二位得到 11000001
,即 193
(正是我在原始示例中看到的)。这种相同的模式(编码字节的值已更改)适用于 129-191
范围内的所有字节(含)。字节 128
和更低的字节不是 NaN,而字节 192
和更高的 是 NaN,但没有修改它们的值,因为它们的第二位(位于最高位尾数位)已经是一个。
这样就回答了为什么会出现这种行为,但在我看来,还有两个问题:
- 是否可以在 C# 中禁用此行为(将信号 NaN 转换为安静)?
- 如果不是,解决方法是什么?
第一个问题的答案似乎是没有(如果我了解到其他情况,我会修改这个答案)。但是,请务必注意,此行为在所有 .NET 版本中并不一致。在我的计算机上,NaN 在我尝试的每个 .NET Framework 版本(从 4.8.0 开始,然后向下工作)上都被转换(即我的编码字节发生了变化)。 .NET Core 3 和 .NET 5(我没有测试每个可用版本)。此外,一位朋友能够在 .NET Framework 4.7.2 上 运行 相同的示例代码,令人惊讶的是,字节在他的机器上 未 修改。不同 C# 的内部结构 运行 次不是我的专业领域,但足以说明版本和计算机之间存在差异。
第二个问题的答案是,正如其他人所建议的那样,完全避免浮点数转换。相反,每组四个字节(在我的例子中代表 RGBA 颜色)可以用整数编码或直接添加到字节数组。