为什么 BitConverter 在转换浮点数和字节时似乎返回不正确的结果?

问题描述

我正在使用 C# 并尝试将四个字节打包成一个浮点数(上下文是游戏开发,其中 RGBA 颜色被打包到单个值中)。为此,我使用了 BitConverter,但某些转换似乎会导致不正确的字节。以下面的例子(使用字节 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,193,255

使用 https://www.h-schmidt.net/FloatConverter/IEEE754.html,我验证了以我开头的四个字节(0,255,相当于二进制 00000000000000001000000111111111)表示浮点值 4.66338115943e-41。通过翻转字节序(二进制 11111111100000010000000000000000),我得到 NaN(与上面代码中的 f 匹配)。但是当我将该浮点数转换回字节时,我得到 0,255(注意 193 当我期待 129 时)。

奇怪的是,使用字节 0,128,255 运行相同的示例是正确的(浮点值 f 变为 -Infinity,然后再次转换回字节产生 0,255)。鉴于这一事实,我怀疑 NaN 是相关的。

有人能解释一下这里发生了什么吗?

更新:问题 Converting 2 bytes to Short in C# 被列为重复项,但这是不准确的。该问题试图将字节转换为值(在这种情况下,两个字节转换为短字节)并且不正确的字节顺序给出了意外的值。就我而言,实际浮点值无关紧要(因为我没有将转换后的值作为浮点数使用)。相反,我试图通过首先转换为浮点数,然后再转换回来,有效地将四个字节直接重新解释为浮点数。如图所示,这种来回有时会返回与我发送的字节不同的字节。

第二次更新:我会简单地提出我的问题。正如 Peter Duniho 评论的那样,BitConverter 永远不会修改您传入的字节,而只是将它们复制到新的内存位置并重新解释结果。但是,正如我的示例所示,可以发送四个字节 (0,255),这些字节在内部复制并重新解释为浮点数,然后将该浮点数转换回与 不同 的字节原件 (0,255)。

BitConverter 相关的字节顺序经常被提及。但是,在这种情况下,我觉得字节序不是根本问题。当我调用 BitConverter.ToSingle 时,我传入了一个包含四个字节的数组。这些字节代表一些转换为浮点数的二进制(32 位)。通过在函数调用之前改变字节序,我所做的就是改变我发送到函数中的位。无论这些位的如何,都应该可以将它们转换为浮点数(也是 32 位),然后将浮点数转换回以获取我发送的相同位.如我的示例所示,使用字节 0,255(二进制 00000000000000001000000111111111)会产生浮点值。我想获取该值(由这些位表示的浮点数)并将其转换为原始的四个字节。

这在 C# 中在所有情况下都可能吗?

解决方法

经过研究、实验和与朋友的讨论,这种行为的根本原因(在与浮点数之间转换时字节发生变化)似乎是 signaling vs. quiet NaNs(正如 Hans Passant 在评论中指出的那样)。我不是信号和安静 NaN 方面的专家,但据我所知,安静 NaN 将尾数的最高位设置为 1,而 信令 NaN 将该位设置为零。请参阅下图(取自 https://www.h-schmidt.net/FloatConverter/IEEE754.html)以供参考。我在每组八位周围绘制了四个彩色框,以及一个指向最高尾数位的箭头。

Visual representation of a float's bit layout.

当然,我发布的问题不是关于浮点位布局或信号与安静的 NaN,而是简单地询问为什么我的编码字节似乎被修改了。答案是 C# 运行时(或者至少我假设它是 C# 运行时)在内部将所有信号 NaN 转换为安静,这意味着 在那里编码的字节位置的第二位从零交换为一

例如,字节 0,129,255(以相反的顺序编码,我认为由于字节序)将值 129 放在第二个字节(绿色框)中。二进制中的 12910000001,因此翻转它的第二位给出 11000001,即 193(正是我在原始示例中看到的)。这种相同的模式(编码字节的值已更改)适用于 129-191 范围内的所有字节。字节 128 和更低的不是 NaN,而字节 192 和更高的 NaN,但没有修改它们的值,因为它们的第二位(放置在最高的 -顺序尾数位)已经是一个。

这样就可以回答为什么会发生这种行为,但在我看来,还有两个问题:

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

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

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