【问题标题】:C "double to num" conversion code: why is it written like this?C“double to num”转换代码:为什么这样写?
【发布时间】:2013-01-07 12:31:08
【问题描述】:

我不明白以下 C 转换函数是如何工作的(以及为什么要这样编写);我相当肯定原作者知道他在做什么:

typedef union TValue {
  uint64_t u64;
  double n;
  struct {
    uint32_t lo;    /* Lower 32 bits of number. */
    uint32_t hi;    /* Upper 32 bits of number. */
  } u32;
  [...]
} TValue;


static int32_t num2bit(double n)
{
  TValue o;
  o.n = n + 6755399441055744.0;  /* 2^52 + 2^51 */
  return (int32_t)o.u32.lo;
}

static uint64_t num2u64(double n)
{
#ifdef _MSC_VER
  if (n >= 9223372036854775808.0)  /* They think it's a feature. */
    return (uint64_t)(int64_t)(n - 18446744073709551616.0);
  else
#endif
  return (uint64_t)n;
}
  • num2bit 实际上只是将double 转换为int32_t 吗?为什么要加法?为什么要这样写?
  • num2u64 中提到的这个“功能”是什么? (我相信 _MSC_VER 意味着它是微软 C 编译器的代码路径)。

请注意,这些函数并非总是使用(取决于 CPU 架构),这是针对 little-endian(我解决了一些预处理器宏以简化)。

在线浏览镜像链接(代码来自LuaJIT项目): 环绕Header file(或whole project)。

感谢每一个提示。

【问题讨论】:

  • “我相当肯定原作者知道他在做什么”从来没有假设过。
  • 别那么不信任......有些人已经赢得了被相信的权利,直到被证明是错误的;)(这是来自一个 JIT 编译器代码库,一个单人项目,目前我所知道的任何动态语言中最快的,所以...)
  • 浮点数位模式的具体知识。阅读 Quake's InvSqrt 了解更多类似这样的脑筋急转弯代码。

标签: c double type-conversion low-level


【解决方案1】:

num2bit:通过将第 51 位和第 52 位设置为 1,这会强制指数为特定数字(否则会溢出) - 然后当您返回 (int32_t)o.u32.lo 时,您知道您得到的是由于指数是固定的,整数返回与双精度的“低 32 位”相同的值。所以,这是一个快速获得大多数双打整数值的技巧。看起来这样做会截断小数点后的数字,如果它开始时是 2^51 或更大,它会产生意想不到的效果。

>>> math.frexp(1.0 + 6755399441055744.0)
(0.7500000000000001, 53)
>>> math.frexp(0.0 + 6755399441055744.0)
(0.75, 53)
>>> math.frexp(564563465 + 6755399441055744.0)
(0.7500000626791358, 53)
>>> math.frexp(-564563465 + 6755399441055744.0)
(0.7499999373208642, 53)
>>> math.frexp(1.5 + 6755399441055744.0)
(0.7500000000000002, 53)
>>> math.frexp(1.6 + 6755399441055744.0)
(0.7500000000000002, 53)
>>> math.frexp(1.4 + 6755399441055744.0)
(0.7500000000000001, 53)

编辑:设置第 51 位和第 52 位的原因是因为如果您只设置第 52 位,那么负数会导致指数发生变化:

>>> math.frexp(0 + 4503599627370496.0)
(0.5, 53)
>>> math.frexp(-543635634 + 4503599627370496.0)
(0.9999998792886404, 52)

num2u64:没有线索。但第一个数字是 2^63,第二个是 2^64。将大于 2^63 的 double 转换为其整数表示时,可能是为了防止溢出或签名失败,但我不能告诉你更多。

【讨论】:

  • tl;dr:这是对double 中的位进行位移的一种奇特方式,因此一个位置是最后一位,同时从符号幅度转换为二进制补码。
【解决方案2】:

这些功能通过魔法“工作”。

这来自 n1570.pdf 的§6.2.6.1p7,这是 C 标准草案:当一个值存储在联合类型对象的成员中时,对象表示的字节不对应于那个成员但确实对应于其他成员采用未指定的值

注意代码如何使用未指定的值,方法是分配给 o.n,然后使用 o.u32.lo 的值。

这来自n1570.pdf的§6.3.1.3p3,这是C标准草案:否则,新类型是有符号的,值不能在其中表示;结果要么是实现定义的,要么引发实现定义的信号。

请注意呈现的代码如何调用实现定义的行为,因为它多次从无符号转换为有符号的 32 位整数。假设它改为引发实现定义的计算异常信号。如果要返回默认信号处理程序,这也会导致未定义的行为。 /* They think it's a feature. */

【讨论】:

  • 看脚注95:“如果用于读取联合对象内容的成员与上次用于在对象中存储值的成员不同,则对象表示的适当部分的值被重新解释为新类型中的对象表示,如 6.2.6 中所述(有时称为“类型双关”的过程)。这可能是一个陷阱表示。”
  • 使用陷阱表示是未定义的行为。
  • uint32_t cannot 有任何陷阱表示(7.20.1.1 p2:“typedef 名称 uintN_t 指定宽度为 N 且没有填充位的无符号整数类型。因此,uint24_t表示这样的无符号整数类型,宽度正好为 24 位。")
  • 很好。我会修改我的帖子。
  • 这个答案没有帮助。正如我在问题中所说,我试图找出代码是如何工作的,而不是最新的 C 标准是否对其行为做出任何断言(完整的 C??-合规性对于JIT 编译器,因为这样的程序本质上需要特定于体系结构/访问 C 标准未涵盖的功能才能高效)。
【解决方案3】:

num2bit 手动将 IEEE 标准 double 的内存中表示转换为 32 位定点二进制补码格式,使用舍入到最接近的整数。

通过union 进行转换是不安全的,因为它违反了严格的类型别名规则。你不能给工会的一个成员写信,然后从另一个成员那里读。做类似的事情会更合适

static int32_t num2bit(double n)
{
  int32_t o;
  n += 6755399441055744.0;  /* 2^52 + 2^51 */
  memcpy( & o, & n, sizeof o ); /* OK with strict aliasing but must mind endianness. */
  return o;
}

这个函数可能是为了优化,但它的价值本身是可疑的。您需要重新测试每一个新的微处理器,并确保它只用于速度更快的硬件。

另请注意,普通 C 浮点整数转换使用四舍五入或截断。这个函数可能根本不打算处理小数值。


num2u64 是特定于 Windows 的解决方法(注意 #ifdef)。当将大于 263double 值转换为无符号整数时,会发生“坏事”(可能是饱和),因此作者减去 264 使其成为负数,然后将其转换为有符号负整数,然后将结果转换为值大于 263 的无符号整数。

在任何情况下,您都可以看出其意图只是将 double 转换为 uint64_t,因为这就是它在非 Windows 平台上所做的一切。

【讨论】:

    【解决方案4】:

    num2bit 旨在实现Lua BitOp semantics,尤其是wrt。模运算。实现定义的行为得到了很好的控制,因为无论如何 LuaJIT 只适用于特定的 CPU、平台和编译器。请勿在其他任何地方使用此代码。

    num2u64 是 MSVC 的错误/错误功能的解决方法,它始终通过 int64_t 将双精度转换为 uint64_t。对于 >= 2^63 的数字,这不会给出所需的结果。 MS 认为这种可憎行为是一种“特征”。呵呵。

    【讨论】:

      猜你喜欢
      • 2013-01-05
      • 1970-01-01
      • 1970-01-01
      • 2014-06-10
      • 1970-01-01
      • 2014-12-30
      • 2012-01-05
      • 2012-09-27
      • 2014-10-14
      相关资源
      最近更新 更多