为什么 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,但没有修改它们的值,因为它们的第二位(位于最高位尾数位)已经是一个。

这样就回答了为什么会出现这种行为,但在我看来,还有两个问题:

  1. 是否可以在 C# 中禁用此行为(将信号 NaN 转换为安静)?
  2. 如果不是,解决方法是什么?

第一个问题的答案似乎是没有(如果我了解到其他情况,我会修改这个答案)。但是,请务必注意,此行为在所有 .NET 版本中并不一致。在我的计算机上,NaN 在我尝试的每个 .NET Framework 版本(从 4.8.0 开始,然后向下工作)上都被转换(即我的编码字节发生了变化)。 .NET Core 3 和 .NET 5(我没有测试每个可用版本)。此外,一位朋友能够在 .NET Framework 4.7.2 上 运行 相同的示例代码,令人惊讶的是,字节在他的机器上 修改。不同 C# 的内部结构 运行 次不是我的专业领域,但足以说明版本和计算机之间存在差异。

第二个问题的答案是,正如其他人所建议的那样,完全避免浮点数转换。相反,每组四个字节(在我的例子中代表 RGBA 颜色)可以用整数编码或直接添加到字节数组。