core-js Float16 方法完全指南:DataView.getFloat16 / setFloat16 与 Math.f16round 的源码级剖析
【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js
core-js 在proposals/float16入口中提供了 TC39 float16array 提案的核心内置方法:DataView.prototype.getFloat16、DataView.prototype.setFloat16与Math.f16round。本文以 docs/web/docs/features/proposals/float16-methods.md 为骨架,逐行剖析 core-js 中这三个方法的实际实现,并结合单元测试与兼容性数据,帮助你理解半精度浮点数的编解码原理、边界行为,以及如何在老环境里安全地启用并验证这些 API。
一、Float16 提案与半精度浮点数背景
Float16(半精度浮点数)是一种 16 位二进制浮点格式,遵循 IEEE 754 的 binary16 布局:
- 1 位符号位(sign)
- 5 位指数位(exponent,偏置 15)
- 10 位尾数位(significand/fraction)
相比 JS 默认的 64 位 double(binary64)与 32 位 float(binary32),Float16 在内存占用上只有 float32 的一半,常用于 AI 推理、WebGPU 纹理、图像处理与游戏引擎的顶点数据等对精度要求不高但对带宽敏感的领域。TC39 的 float16array 提案(packages/core-js/proposals/float16.js源码注释指向该提案)不仅提出了Float16Array等类型化数组,还定义了与之配套的DataView读写方法与Math舍入函数——这正是 core-js 在本入口中实现的部分。
需要先说明一个容易踩坑的细节:原始文档的签名表中写的是Math.fround,但该提案实际要求的方法是Math.f16round(Math.fround是 ES2015 中早已存在的 float32 舍入方法)。core-js 源码实现的是Math.f16round,下文全部以实际实现为准。
二、内置方法签名与入口点
2.1 内置方法签名
以下是 core-js 实际提供的三个方法(与文档签名一致,其中littleEndian默认值为false,即默认按大端序读写):
class DataView { getFloat16(offset: any, littleEndian?: boolean = false): number setFloat16(offset: any, value: any, littleEndian?: boolean = false): void; } namespace Math { f16round(number: any): number; }关于littleEndian默认值的细节,可以从实现中验证:在 es.data-view.get-float16.js 中,getUint16(this, byteOffset, arguments.length > 1 ? arguments[1] : false)明确以false作为缺省参数,与签名声明完全一致。
2.2 入口点
文档给出的入口为:
core-js/proposals/float16实际入口文件 packages/core-js/proposals/float16.js 会依次引入三个底层模块:
require('../modules/esnext.data-view.get-float16'); require('../modules/esnext.data-view.set-float16'); require('../modules/esnext.math.f16round');而这三个esnext模块目前只是es稳定模块的转发(源码注释标注TODO: Remove from core-js@4),例如 esnext.math.f16round.js 实际就是require('../modules/es.math.f16round')。因此你也可以按需精确引入对应的稳定入口:
import 'core-js/proposals/float16'; // 一次性引入全部三个方法 // 或者按需引入稳定模块: import 'core-js/stable/data-view/get-float16'; import 'core-js/stable/data-view/set-float16'; import 'core-js/stable/math/f16round';三、DataView.prototype.getFloat16:读回半精度字节
getFloat16(byteOffset, littleEndian)的作用是从DataView的指定字节偏移处读取一个 16 位无符号整数,再按 Float16 布局解包为 JSNumber。
3.1 解包实现(unpackFloat16)
es.data-view.get-float16.js 的核心是unpackFloat16函数:
var EXP_MASK16 = 31; // 2 ** 5 - 1,指数位全 1 掩码 var SIGNIFICAND_MASK16 = 1023; // 2 ** 10 - 1,尾数位全 1 掩码 var MIN_SUBNORMAL16 = pow(2, -24); // 最小正次规格化数 2 ** -10 * 2 ** -14 var SIGNIFICAND_DENOM16 = 0.0009765625; // 2 ** -10 var unpackFloat16 = function (bytes) { var sign = bytes >>> 15; var exponent = bytes >>> 10 & EXP_MASK16; var significand = bytes & SIGNIFICAND_MASK16; if (exponent === EXP_MASK16) return significand === 0 ? sign === 0 ? Infinity : -Infinity : NaN; if (exponent === 0) return significand * (sign === 0 ? MIN_SUBNORMAL16 : -MIN_SUBNORMAL16); return pow(2, exponent - 15) * (sign === 0 ? 1 + significand * SIGNIFICAND_DENOM16 : -1 - significand * SIGNIFICAND_DENOM16); };这个函数完整覆盖了 Float16 的四种数值形态:
| 情形 | 判定条件 | 返回值 |
|---|---|---|
| 无穷 | 指数位全 1 且尾数为 0 | ±Infinity |
| NaN | 指数位全 1 且尾数非 0 | NaN |
| 次规格化数 | 指数位全 0 | significand * 2^-24(带符号) |
| 规格化数 | 其余情况 | 2^(exponent-15) * (1 + significand * 2^-10) |
其中bytes >>> 15取出符号位,bytes >>> 10 & 31取出 5 位指数,bytes & 1023取出 10 位尾数。DataView.prototype.getUint16通过uncurryThis解包后直接复用,保证字节序(大端/小端)语义与原生getUint16完全一致。
四、DataView.prototype.setFloat16:把 Number 打包为半精度字节
setFloat16(byteOffset, value, littleEndian)先将 JSNumber转换为 Float16 位模式,再写入DataView。
4.1 打包实现(packFloat16)
es.data-view.set-float16.js 实现了packFloat16,并处理了舍入进位等细节:
var MIN_INFINITY16 = 65520; // (2 - 2 ** -11) * 2 ** 15,超出即视为无穷 var MIN_NORMAL16 = 0.000061005353927612305; // (1 - 2 ** -11) * 2 ** -14,最小规格化数附近边界 var REC_MIN_SUBNORMAL16 = 16777216; // 2 ** 10 * 2 ** 14 var REC_SIGNIFICAND_DENOM16 = 1024; // 2 ** 10 var packFloat16 = function (value) { if (value !== value) return 0x7E00; // NaN → 默认 NaN 位模式 if (value === 0) return (1 / value === -Infinity) << 15; // 保留 ±0 符号 var neg = value < 0; if (neg) value = -value; if (value >= MIN_INFINITY16) return neg << 15 | 0x7C00; // 溢出 → ±Infinity if (value < MIN_NORMAL16) return neg << 15 | roundTiesToEven(value * REC_MIN_SUBNORMAL16); // 次规格化 // 规格化路径:计算指数与尾数,处理 2^-15 边界与尾数进位 var exponent = floor(log2(value)); if (exponent === -15) return neg << 15 | REC_SIGNIFICAND_DENOM16; var significand = roundTiesToEven((value * pow(2, -exponent) - 1) * REC_SIGNIFICAND_DENOM16); if (significand === REC_SIGNIFICAND_DENOM16) return neg << 15 | exponent + 16 << 10; return neg << 15 | exponent + 15 << 10 | significand; };几个值得注意的工程细节:
- NaN 规范化:所有 NaN 统一编码为
0x7E00,与规范要求一致; - ±0 符号保留:通过
1 / value === -Infinity区分-0; - 舍入策略:使用
roundTiesToEven(银行家舍入)处理次规格化数与规格化数的尾数舍入; - 进位处理:当尾数舍入后达到
1024(即需要进位到下一指数级)时,指数+1并清空尾数,避免双重进位错误; - 参数校验:写入前使用
aDataView(this)校验接收者必须是DataView实例,使用toIndex(byteOffset)把偏移量规范化为数组索引。
4.2 边界数值速查表
结合 es.math.f16round.js 中定义的常量,Float16 的关键边界值如下:
| 常量 | 数值 | 说明 |
|---|---|---|
FLOAT16_MAX_VALUE | 65504 | 最大有限值(2 - 2^-11) * 2^15 |
FLOAT16_MIN_VALUE | 6.103515625e-05 | 最小正规格化数2^-14 |
FLOAT16_EPSILON | 0.0009765625 | 机器精度2^-10 |
| — | 5.960464477539063e-08 | 最小正次规格化数2^-24 |
| — | 0x7C00 / 0xFC00 | ±Infinity位模式 |
| — | 0x7E00 | 默认 NaN 位模式 |
五、Math.f16round:一步到位的半精度舍入
Math.f16round(x)把输入值舍入到最接近的 Float16 可表示值,等价于"转成半精度再读回"。
5.1 实现与内部工具函数
es.math.f16round.js 本身极薄,逻辑复用自通用浮点舍入工具 internals/math-float-round.js:
module.exports = function (x, FLOAT_EPSILON, FLOAT_MAX_VALUE, FLOAT_MIN_VALUE) { var n = +x; var absolute = abs(n); var s = sign(n); if (absolute < FLOAT_MIN_VALUE) return s * roundTiesToEven(absolute / FLOAT_MIN_VALUE / FLOAT_EPSILON) * FLOAT_MIN_VALUE * FLOAT_EPSILON; var a = (1 + FLOAT_EPSILON / EPSILON) * absolute; var result = a - (a - absolute); if (result > FLOAT_MAX_VALUE || result !== result) return s * Infinity; return s * result; };算法要点:
- 次规格化路径:当绝对值小于最小规格化数
2^-14时,先按2^-24的粒度做 ties-to-even 舍入,落在次规格化区间; - 规格化路径:通过
a - (a - absolute)的技巧在 double 运算中模拟单精度级舍入(EPSILON即Number.EPSILON); - 溢出处理:舍入结果超过
65504或产生 NaN 时返回±Infinity; - 零与负零:
sign与符号处理保留了-0的语义。
正因为同一套floatRound工具函数被 float32/float16 等不同精度复用,Math.f16round与规范要求的语义可以保持高度一致,这也是 core-js 把舍入逻辑下沉到 internals 的原因。
六、实战示例:读写 Float16 数据
以下示例可以直接在支持 ES Modules 的环境中使用,先引入入口,再完成编码、写入、读回与舍入验证:
import 'core-js/proposals/float16'; const buffer = new ArrayBuffer(4); const view = new DataView(buffer); // 大端序写入(littleEndian 缺省为 false) view.setFloat16(0, 1.337); console.log(view.getUint16(0).toString(16)); // 0x3d59(1.337 的 Float16 位模式) console.log(view.getFloat16(0)); // 1.3369140625(半精度舍入结果) // 小端序写入 view.setFloat16(2, -0.5, true); console.log(view.getFloat16(2, true)); // -0.5 // Math.f16round 与 set/get 结果一致 console.log(Math.f16round(1.337)); // 1.3369140625 console.log(Math.f16round(-0)); // -0 console.log(Math.f16round(65504)); // 65504(最大有限值) console.log(Math.f16round(65505)); // Infinity(溢出) console.log(Math.f16round(5.96e-8 / 2)); // 0(小于最小次规格化数一半)单元测试 tests/unit-global/es.math.f16round.js 对上述边界行为做了系统断言:f16round()与f16round(undefined)返回NaN、f16round(null)返回0、f16round(-0)返回-0、f16round(Number.MAX_VALUE)返回Infinity、f16round(1.337)精确等于1.3369140625、f16round(7.9999999)进位为8等。这些断言同样适用于setFloat16/getFloat16组合的读写一致性验证。
七、兼容性判断与 Polyfill 时机
在决定是否引入 polyfill 前,应判断运行环境是否已原生支持这些方法。core-js-compat 的浏览器兼容数据维护在 packages/core-js-compat/src/data.mjs 中:
es.data-view.get-float16/es.data-view.set-float16:Chrome 135、Firefox 129、Safari 18.2、Deno 1.43、Bun 1.1.23 起原生支持;es.math.f16round:Chrome 135、Firefox 129、Safari 18.2、Deno 1.43、Bun 1.1.23、Rhino 1.9.0 起原生支持(详见该文件第 540、547、974 行附近)。
也就是说,如果目标环境低于上述版本,就需要通过core-js/proposals/float16入口补齐这三个方法。core-js-compat 包(index.js)支持按目标浏览器列表(browserslist)自动计算需要引入的模块清单,可与core-js-builder配合生成最小化的按需构建产物。
八、相关文件导航
想要深入钻研或复现本文结论,可以重点阅读以下仓库文件:
- 入口与转发:proposals/float16.js、esnext.data-view.get-float16.js、esnext.math.f16round.js
- 核心实现:es.data-view.get-float16.js、es.data-view.set-float16.js、es.math.f16round.js
- 通用舍入工具:math-float-round.js
- 边界校验辅助:to-index.js 与 a-data-view.js(位于
packages/core-js/internals/目录) - 测试用例:tests/unit-global/es.math.f16round.js
- 兼容性数据:packages/core-js-compat/src/data.mjs
- 稳定入口清单:
packages/core-js/stable/data-view/与packages/core-js/stable/math/目录下的get-float16.js、set-float16.js、f16round.js
九、小结
DataView.prototype.getFloat16/setFloat16与Math.f16round是 TC39 float16array 提案中可直接独立使用的一组能力:前者完成半精度字节与 JSNumber之间的双向转换(默认大端序,支持小端序参数),后者提供等价的纯数值舍入。core-js 通过 proposals/float16.js 一个入口把它们全部引入,实现上分别以unpackFloat16、packFloat16与共享的floatRound工具函数承载核心逻辑,并通过roundTiesToEven保证舍入符合 IEEE 754 语义。配合 compat 数据 与 单元测试,你可以准确地判断目标环境是否需要 polyfill,并验证接入后的行为是否符合规范。
【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考