【问题标题】:Why does BitConverter seemingly return incorrect results when converting floats and bytes?为什么在转换浮点数和字节时,BitConverter 似乎返回不正确的结果?
【发布时间】:2021-05-09 00:55:13
【问题描述】:

我正在使用 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# 被列为重复问题,但这是不准确的。该问题试图将字节转换为一个值(在这种情况下,将两个字节转换为一个短字节)并且不正确的字节序给出了一个意外的值。在我的情况下,实际的浮点值是无关紧要的(因为我没有使用转换后的值作为浮点数)。相反,我试图通过首先转换为浮点数,然后再转换回来,将四个字节有效地重新解释为浮点数直接。如图所示,这种来回有时会返回与我发送的字节不同的字节。

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

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

在所有情况下都可以在 C# 中实现吗?

【问题讨论】:

  • BitConverter 字节顺序可以通过IsLittleEndian 属性确定。至于你的具体例子,不清楚为什么如果你想要 RGBA 值你使用float,但你观察到的基本原因是有多个(多于一个)二进制值被处理NaNInfinity-Infinity。请记住,字节顺序会影响 byte 顺序,但不会影响位顺序。当您反转 时,您没有纠正字节顺序问题,而是破坏了值,因此使用 193 而不是 129
  • 我仍然认为这不是根本问题(列出的重复项与我的问题相同)。即使考虑到字节序,一些 RGBA 字节是否最终会导致我在上面使用的值,这样转换为浮点数并返回字节会产生不正确的结果?归根结底,我试图将 32 位转换为浮点数并返回到相同的位。这在所有情况下都可以在 C# 中实现吗?
  • @Grimelios floatSystem.Single 的别名,它是一个值类型。将您的 NaN 值作为参数传递给 BitConverter.GetBytes() 通过值而不是引用传递它,这意味着复制的值可能会使用不同的位序列使用 NaN 值初始化。只要指数是 FF 并且分数不为零,float 将是 NaN。所以,将四个字节转换为float 并返回到原始的四个字节在所有情况下都是不可能的。正如彼得已经说过的,这种转换为浮动和返回的动机尚不清楚。
  • @PeterDuniho 感谢您指出BitConverter 并不关心NaN 有多种表示形式这一事实。如果您真的重新阅读我所说的话,我从未声称它确实如此。 “传递给BitConverter 的4 个字节将直接复制到float 值中,然后如果再次传递给BitConverter 以转换回数组,将再次逐字复制回来。” 事实上,this isn't really true。他们期望的结果与实际结果不符;确切的语义并不重要。
  • 是的,NaN 是一个问题。该标准为 NaN 指定了一系列不同的值,区分安静的 NaN 和 signaling NaNs。坏主意,.NET 不支持它只允许 NaN 的单个值,您会看到它使值正常化。其他情况是 +0 和 -0 之间的区别,.NET 规范化为 +0。从技术上讲,非规范值可以刷新为 0(不应该发生)。取得成功的唯一方法是不这样做,BitConverter.ToInt32() 不会被弄乱。

标签: c# endianness bitconverter


【解决方案1】:

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

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

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

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

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

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

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

【讨论】:

猜你喜欢
  • 2021-03-14
  • 2011-08-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多