我想知道 JS_CANONICALIZE_NAN 的目的是什么,是否所有平台都需要它?
1 回答
这是一个有趣的!因此,SpiderMonkey 在内部使用标记值表示来表示 JavScript 的“无类型值”——这允许 VM 确定诸如“存储的变量a
是数字,存储的值b
是数字,所以运行a + b
会进行数字加法”之类的事情.
有许多不同的值标记方案,SpiderMonkey 使用一种称为“NaN 装箱”的方案。这意味着引擎中的所有无类型值都由 64 位值表示,这些值可以是:
- 双倍,或
- 一个标记的非双精度,位于 IEEE 双精度浮点值的“NaN 空间”中。
这里真正的诀窍是现代系统通常使用单个位模式来表示 NaN,您可以将其作为 math.h'ssqrt(-1)
或log(0)
. 但是根据 IEEE 浮点规范,有很多位模式也被认为是 NaN。
double 由以下子字段组成:
{sign: 1, exponent: 11, significand: 52}
NaN 通过用 1 填充指数字段并在有效数字中放置一个非零值来表示。
如果你运行一个像这样的小程序来查看你平台的 NaN 值:
#include <stdio.h>
#include <math.h>
#include <limits>
static unsigned long long
DoubleAsULL(double d) {
return *((unsigned long long *) &d);
}
int main() {
double sqrtNaN = sqrt(-1);
printf("%5f 0x%llx\n", sqrtNaN, DoubleAsULL(sqrtNaN));
double logNaN = log(-1);
printf("%5f 0x%llx\n", logNaN, DoubleAsULL(logNaN));
double compilerNaN = NAN;
printf("%5f 0x%llx\n", compilerNaN, DoubleAsULL(compilerNaN));
double compilerSNAN = std::numeric_limits<double>::signaling_NaN();
printf("%5f 0x%llx\n", compilerSNAN, DoubleAsULL(compilerSNAN));
return 0;
}
你会看到这样的输出:
-nan 0xfff8000000000000 // Canonical qNaNs...
nan 0x7ff8000000000000
nan 0x7ff8000000000000
nan 0x7ff4000000000000 // sNaN (signaling)
请注意,安静 NaN 的唯一区别在于符号位,后跟 12 位 1,满足上述 NaN 要求。最后一个,信号 NaN,清除第 12 个(is_quiet)NaN 位并启用第 13 个以保持上述 NaN 不变。
除此之外,NaN 空间可以自由使用——11 位来填充指数,确保有效位非零,并且你还有很多空间。在 x64 上,我们使用 47 位虚拟地址假设,这为我们留下了64 - 47 - 11 = 6
用于注释值类型的位。在 x86 上,所有对象指针都适合低 32 位。
但是,我们仍然需要确保非规范的 NaN,如果它们通过 js-ctypes 之类的东西潜入,不会产生看起来像标记的非双精度值的东西,因为这可能导致 VM 中的可利用行为。(将数字视为对象是非常糟糕的消息。)因此,当我们形成双精度数时(如在 DOUBLE_TO_JSVAL 中),我们确保将所有双精度数d != d
规范化为规范的 NaN 形式。
更多信息在错误 584168中。