首页
/
文章
/
Float16Array 2026 年成为 TC39 基线,JS 开发者在机器学习与图形计算中的需求
TC39 Float16Array 已成为 2026 年基线:JavaScript 开发者在 ML 和图形工作负载中需要了解的内容
本文在人工监督和审阅下,得到了 AI 的协助撰写。
JavaScript 新兴的 ML 和图形工作负载中的大多数性能问题源于在不匹配的数值格式之间复制数据。团队编写工作线程来解码 Float32 张量缓冲区,将其发送给期望半精度的 WebGPU 着色器,随后好奇为什么带宽在计算之前就饱和了。编译器保持沉默。着色器运行。内存压力加倍。
Float16Array 在 2024 年达到 TC39 第 4 阶段,今年获得 Baseline 2026 状态。它提供原生的 16 位 IEEE 754 半精度浮点数,支持直接的 TypedArray 操作和 DataView 集成。当工程师匹配硬件实际需要的精度时,带宽会减半,计算利用率会提升。
这一区别至关重要。WebGPU 着色器、ONNX 运行时量化以及 HDR 颜色缓冲区均以原生半精度运行。在 Float16Array 出现之前,每个 JavaScript 代码库要么使用 Float32 浪费带宽,要么编写自定义二进制打包器。如今语言自行处理格式转换。
关键要点
Float16Array 存储具有 10 位尾数和 5 位指数的 16 位 IEEE 754 半精度浮点数,在精度代价(±65504 范围,3-4 位小数)下将内存和带宽成本减半。
WebGPU 计算着色器和 ML 推理管道通过匹配原生半精度格式即可获得性能提升,从而消除了 GPU 驱动内部的 Float32 → Float16 转换开销。
DataView 新增了 getFloat16 和 setFloat16 方法,用于二进制协议解析,取代了脆弱的手动位打包例程。
Float32Array 仍是通用数值工作的默认选择;当内存带宽成为瓶颈且工作负载能够容忍降低的精度时,Float16Array 才显得重要。
Baseline 2026 表示生产使用今天是安全的,但旧环境需要 polyfills,这些 polyfills 在 Uint16Array 上模拟操作,带来性能损耗。
理解 16 位浮点数:内存 vs 精度权衡
Float16Array 以 IEEE 754 半精度格式表示数字。每个值占用 16 位:1 位符号位,5 位指数位,10 位尾数位。指数范围为 -14 到 +15(偏置表示),可表示的范围大约为 ±65504。10 位尾数提供大约 3 到 4 位十进制精度。
精度损失是即时且不可协商的。Float32 值 3.14159265 在 Float16 中变为 3.140625。算术运算会叠加舍入误差。这不是缺陷。该格式之所以存在,是因为某些工作负载会以精度换取吞吐量。
机器学习推理和图形渲染是典型用例。使用 Float32 权重训练的神经网络在部署时常量化为 Float16,因为额外的精度对最终预测准确度没有贡献。同样,HDR 颜色缓冲区需要宽动态范围,但不需要 Float32 的 23 位尾数。10 位尾数足以满足感知颜色差异的需求。
内存优势显而易见。100 万元素的 Float32Array 消耗 4 MB。相同数据在 Float16Array 中仅需 2 MB。当该数组在 CPU 与 GPU 之间通过 PCIe 传输,或在网络中传输时,带宽成本减半。GPU 着色器内的算术单元原生以半精度运行,因此驱动在计算过程中不会将值提升回 Float32。整个流水线因此更窄且更快。
开发者必须认识到何时需要精度。金融计算、需要亚像素精度的坐标变换以及会累积误差的迭代求解器在使用 Float16 时都会失效。这不仅仅是性能偏好的问题,而是数学正确性的问题。如果算法依赖于超过 3 位十进制精度,Float16 会得到错误结果。
创建和使用 Float16Array:基本操作
Float16Array 的构造方式与 Float32Array 和 Float64Array 相同。开发者可以通过长度、ArrayBuffer、可迭代对象或复制另一个 TypedArray 来创建实例。其 API 接口与其他类型数组保持一致,因此从 Float32 迁移非常直接。
// Create a new Float16Array with 5 elements
const tensor = new Float16Array ( 5 );
tensor [ 0 ] = 1.5 ;
tensor [ 1 ] = - 3.14159 ;
tensor [ 2 ] = 0.001 ;
tensor [ 3 ] = 65000 ;
tensor [ 4 ] = 100000 ; // exceeds max representable value
console . log ( tensor );
// Float16Array(5) [ 1.5, -3.140625, 0.0009765625, 65504, Infinity ]
// Create from existing Float32 data
const float32Data = new Float32Array ([ 2.5 , 3.7 , 4.9 ]);
const quantized = new Float16Array ( float32Data );
console . log ( quantized );
// Float16Array(3) [ 2.5, 3.69921875, 4.89453125 ]
// Share an underlying ArrayBuffer
const buffer = new ArrayBuffer ( 16 );
const view16 = new Float16Array ( buffer );
const view32 = new Float32Array ( buffer );
view16 [ 0 ] = 1.5 ;
console . log ( view32 [ 0 ]);
// Output depends on byte interpretation — demonstrates shared storage
量化在赋值过程中自动进行。当出现超出可表示范围的值时,Float16Array 会将其钳位到 ±65504,或将其表示为 Infinity。精度超过 10 位的值会四舍五入到最近的可表示 Float16。整个过程不会产生警告或错误,这种静默截断正是设计所致。
类型化数组的方法无需修改即可使用。map、filter、reduce、slice 和 subarray 均能按预期工作。唯一的限制是,每次产生新 Float16Array 的操作都会对结果进行量化。相同的算术步骤链相比 Float32 会更快地累积舍入误差。
const original = new Float16Array ([ 1.1 , 2.2 , 3.3 , 4.4 , 5.5 ]);
const doubled = original . map ( x => x * 2 );
console . log ( doubled );
// Float16Array(5) [ 2.19921875, 4.3984375, 6.59765625, 8.796875, 11 ]
const sum = original . reduce (( acc , val ) => acc + val , 0 );
console . log ( sum );
// 16.5 (accumulated rounding)
const slice = original . slice ( 1 , 4 );
console . log ( slice );
// Float16Array(3) [ 2.19921875, 3.30078125, 4.3984375 ]
遍历方式与其他类型化数组完全一致。for...of、解构和展开语法都能照常使用。唯一区别是:取出的每个值都带有 Float16 精度。
Float16Array、Float32Array 与 Float64Array:分别该用在什么场景
在 Float16Array、Float32Array 和 Float64Array 之间做选择,要权衡精度需求、内存限制和硬件能力。三者各有分工,选错了格式,要么浪费资源,要么得出错误结果。
Float64Array 是科学计算、金融运算以及需要亚毫米级精度的坐标系统的默认选择。要迭代数千次的模拟,需要额外的尾数位来防止误差累积。在正确性优先的场景下,每元素 8 字节的存储开销完全可以接受。JavaScript 的 Number 运算内部大多基于 Float64,因此 Float64Array 在纯 JS 计算中还能避免类型转换开销。
Float32Array 覆盖了大部分图形、音频处理和常规数值工作。WebGL 缓冲区、Web Audio 节点和物理引擎默认都以 Float32 运行。4 字节的大小在精度和内存之间取得了平衡。6 到 9 位十进制精度的尾数,足以应付渲染变换、光照计算和信号处理。当精度重要但 Float64 又过于奢侈时,Float32 就是主力格式。
Float16Array 面向的是带宽受限且精度可放宽的流水线。运行在移动 GPU 上的 WebGPU 计算着色器立刻就能受益——硬件执行半精度运算的吞吐量是 Float32 的两倍。神经网络推理同样能获益:只要模型本身采用混合精度训练,或是训练后做过量化。WebGL 中的 HDR 帧缓冲需要宽动态范围来做色调映射,但并不要求 23 位色深精度。
踩坑的方式很隐蔽。开发者把 Float32 物理模拟迁移到 Float16 以节省内存后,会遇到物体漂移、碰撞失效、积分器发散——精度损失破坏了算法赖以成立的数值稳定性假设。反过来,把 Float32 张量发给期望 Float16 的 WebGPU 着色器,则会浪费带宽。GPU 驱动会在内部做转换,但 PCIe 传输量和内存占用依然是原来的两倍。
这意味着格式选择首先是正确性决策,其次才是优化。先以 Float32 作为默认。只有在性能分析显示内存或带宽压力且工作负载能够容忍精度下降时,才转向 Float16。对于在 Float32 中已证明数值不稳定的算法,保留使用 Float64。
机器学习工作负载:WebGPU 集成与张量运算
WebGPU 计算着色器以半精度原生执行张量运算。当 JavaScript 代码加载预训练模型并进行推理时,将张量格式与着色器的期望匹配可以消除转换开销。Float16Array 使这种直接交互成为可能。
这一模式在 ONNX Runtime Web、带有 WebGPU 后端的 TensorFlow.js 以及自定义计算管道中均可见。使用自动混合精度在 PyTorch 中训练的模型会生成 Float16 权重。导出的 ONNX 图指定了半精度运算。JavaScript 代码将这些权重反序列化为 Float16Array,并直接上传到 GPU 缓冲区,无需格式转换。
// Load pre-quantized model weights
async function loadModelWeights ( url : string ): Promise < Float16Array > {
const response = await fetch ( url );
const buffer = await response . arrayBuffer ();
return new Float16Array ( buffer );
}
// Create WebGPU buffer from Float16 tensor
async function uploadTensorToGPU (
device : GPUDevice ,
tensor : Float16Array
): Promise < GPUBuffer > {
const buffer = device . createBuffer ({
size : tensor . byteLength ,
usage : GPUBufferUsage . STORAGE | GPUBufferUsage . COPY_DST ,
});
device . queue . writeBuffer ( buffer , 0 , tensor );
return buffer ;
}
// Inference pipeline
async function runInference (
device : GPUDevice ,
weights : Float16Array ,
input : Float16Array
): Promise < Float16Array > {
const weightBuffer = await uploadTensorToGPU ( device , weights );
const inputBuffer = await uploadTensorToGPU ( device , input );
const outputBuffer = device . createBuffer ({
size : input . byteLength ,
usage : GPUBufferUsage . STORAGE | GPUBufferUsage . COPY_SRC ,
});
// Shader execution omitted for brevity — assume GEMM kernel
// reads from weightBuffer and inputBuffer, writes to outputBuffer
// Read result back
const resultBuffer = device . createBuffer ({
size : input . byteLength ,
usage : GPUBufferUsage . MAP_READ | GPUBufferUsage . COPY_DST ,
});
const commandEncoder = device . createCommandEncoder ();
commandEncoder . copyBufferToBuffer (
outputBuffer , 0 ,
resultBuffer , 0 ,
input . byteLength
);
device . queue . submit ([ commandEncoder . finish ()]);
await resultBuffer . mapAsync ( GPUMapMode . READ );
const result = new Float16Array ( resultBuffer . getMappedRange (). slice ( 0 ));
resultBuffer . unmap ();
return result ;
}
带宽节省是可测量的。将 1 亿参数的模型量化为 Float16 后占用 200 MB,而非 400 MB。在 PCIe 4.0 上,从系统内存到 GPU 内存的传输在理论峰值带宽下从 40 ms 降至 20 ms。统一内存架构的移动设备仍能受益,因为缓存行能够容纳两倍的数据。
在推理过程中,精度损失是可以接受的,因为模型在训练阶段已经学到了鲁棒特征。一个预测 "cat" 置信度为 0.87 的分类网络与置信度为 0.869 的网络在实际中没有区别。同样的容忍度不适用于训练。反向传播在数千步中累积梯度,若不采用混合精度技术并在 Float32 中保存主权重,Float16 精度会导致发散。
故障模式是静默的。当开发者将 Float32 张量上传到期望 Float16 的着色器时,会看到流水线正常运行。驱动会在内部转换数据。性能会下降,但不会导致崩溃。只有在工程师检查内存带宽利用率时,性能分析才会显露出转换开销。
图形和 WebGL:颜色缓冲区和 HDR 渲染
WebGL 和 WebGPU 在 HDR 帧缓冲区和光照计算中广泛使用 Float16。线性颜色空间中强度为 10.0(比白色亮十倍)的颜色值能够轻松容纳在 Float16 的 ±65504 范围内。10 位尾数在色调映射后提供了足够的精度来区分感知颜色差异。
这种模式出现在延迟渲染管线、 bloom 效果以及 PBR 工作流中。片段着色器将 HDR 颜色写入 Float16 渲染目标。后处理通道读取这些值,应用色调映射,并将 LDR 颜色输出到 8 位后帧缓冲区。Float16 提供了中间范围,且无需承受 Float32 的内存开销。
JavaScript 代码在上传纹理数据或从渲染目标读取像素时会与 Float16 交互。浏览器将这些缓冲区暴露为 TypedArrays。如果没有 Float16Array,开发者只能编写手动转换例程,或使用 Float32 并接受带宽浪费。
// Create an HDR texture from Float16 data
function createHDRTexture (
gl : WebGL2RenderingContext ,
width : number ,
height : number ,
data : Float16Array
): WebGLTexture {
const texture = gl . createTexture () ! ;
gl . bindTexture ( gl . TEXTURE_2D , texture );
// EXT_color_buffer_half_float enables Float16 render targets
const ext = gl . getExtension ( ' EXT_color_buffer_half_float ' );
if ( ! ext ) {
throw new Error ( ' Float16 render targets not supported ' );
}
gl . texImage2D (
gl . TEXTURE_2D ,
0 ,
gl . RGBA16F ,
width ,
height ,
0 ,
gl . RGBA ,
gl . HALF_FLOAT ,
data
);
gl . texParameteri ( gl . TEXTURE_2D , gl . TEXTURE_MIN_FILTER , gl . LINEAR );
gl . texParameteri ( gl . TEXTURE_2D , gl . TEXTURE_MAG_FILTER , gl . LINEAR );
return texture ;
}
// Read HDR pixels from a framebuffer
function readHDRPixels (
gl : WebGL2RenderingContext ,
x : number ,
y : number ,
width : number ,
height : number
): Float16Array {
const pixels = new Float16Array ( width * height * 4 );
gl . readPixels (
x , y , width , height ,
gl . RGBA ,
gl . HALF_FLOAT ,
pixels
);
return pixels ;
}
// Generate HDR environment map
function generateHDREnvironment ( size : number ): Float16Array {
const data = new Float16Array ( size * size * 4 );
for ( let y = 0 ; y < size ; y ++ ) {
for ( let x = 0 ; x < size ; x ++ ) {
const idx = ( y * size + x ) * 4 ;
const u = x / size ;
const v = y / size ;
// Simple gradient with HDR range
data [ idx + 0 ] = u * 10.0 ; // R: 0-10
data [ idx + 1 ] = v * 5.0 ; // G: 0-5
data [ idx + 2 ] = 2.0 ; // B: constant
data [ idx + 3 ] = 1.0 ; // A: opaque
}
}
return data ;
}
在实际使用中,精度限制很少被注意到。线性空间中颜色相差 0.001,经过伽马校正后在屏幕上通常不可察觉。只有在非常暗或非常亮的区域才会出现渐变带状现象。如果一个天空盒的强度范围是 0.01 到 100.0,而色调映射将其压缩到 0-255,Float16 只有 3-4 位有效小数位,可能会出现可见的阶梯。解决办法是使用抖动,而不是提高精度。
采用 tile-based 渲染的移动 GPU 在使用 Float16 渲染目标时能获得显著收益。当格式更窄时,整个 tile 能够完全放入片上内存。一个 16x16 的 RGBA16F tile 占用 2 KB,而 RGBA32F 需要 4 KB。这一差别决定了 tile 是否会溢出到主内存,从而导致流水线停滞。
DataView 方法:getFloat16 和 setFloat16 用于二进制数据
DataView 同时获得了 getFloat16 和 setFloat16 方法,配合 Float16Array 使用。这些方法能够在任意 ArrayBuffer 中解析和序列化 16 位浮点数,这在处理二进制协议、文件格式或与原生代码互操作时尤为重要。
这一模式在 glTF 解析器、网络消息解码器以及数据结构序列化器中常见。二进制文件在字节偏移 128 处指定了 Float16 顶点位置。JavaScript 通过 getFloat16 读取这些值,从而避免了手动位操作。
// Parse Float16 values from a binary protocol
function parseFloat16Message ( buffer : ArrayBuffer ): number [] {
const view = new DataView ( buffer );
const count = view . getUint32 ( 0 , true ); // little-endian count
const values : number [] = [];
for ( let i = 0 ; i < count ; i ++ ) {
const offset = 4 + i * 2 ; // skip 4-byte header, 2 bytes per Float16
values . push ( view . getFloat16 ( offset , true ));
}
return values ;
}
// Serialize Float16 data to binary format
function serializeFloat16Array ( data : number []): ArrayBuffer {
const buffer = new ArrayBuffer ( 4 + data . length * 2 );
const view = new DataView ( buffer );
view . setUint32 ( 0 , data . length , true ); // write count
for ( let i = 0 ; i < data . length ; i ++ ) {
view . setFloat16 ( 4 + i * 2 , data [ i ], true );
}
return buffer ;
}
// Read mixed-format binary structure
interface SensorReading {
timestamp : number ; // Float64
temperature : number ; // Float16
humidity : number ; // Float16
pressure : number ; // Float32
}
function parseSensorReading ( buffer : ArrayBuffer , offset : number ): SensorReading {
const view = new DataView ( buffer );
return {
timestamp : view . getFloat64 ( offset , true ),
temperature : view . getFloat16 ( offset + 8 , true ),
humidity : view . getFloat16 ( offset + 10 , true ),
pressure : view . getFloat32 ( offset + 12 , true ),
};
}
字节序参数在解析来自外部系统的文件时很重要。glTF 使用小端编码。某些二进制格式使用大端。getFloat16 和 setFloat16 的第二个参数以与 getFloat32 相同的方式指定字节序。
在 Float16Array 和 DataView 方法出现之前,开发者编写手动打包器,在 Float16 与 Uint16 位模式之间进行转换。这些例程既脆弱又慢。原生方法完全消除了这段代码。
精度行为与 Float16Array 相同。从 DataView 读取 Float16 会得到一个值被量化到 Float16 精度的 JavaScript Number。写入大于 ±65504 的值会被夹紧或产生 Infinity。不会有错误或警告。
常见问题
在生产代码中何时应使用 Float16Array 而非 Float32Array?
当内存带宽被证明是瓶颈且工作负载能够容忍 3-4 位小数精度时,使用 Float16Array。运行量化机器学习模型和 HDR 图形流水线的 WebGPU 计算着色器是主要使用场景。对于一般的数值计算,默认使用 Float32Array。
Float16Array 在 CPU 上还是仅在 GPU 上能提升性能?
CPU SIMD 单元(x86 上的 AVX-512 FP16,ARM 上的 ARMv8.2-FP16)能够更快地执行半精度运算,但 JavaScript 引擎尚未在 Float16Array 实现中暴露此特性。性能提升目前仅限于 GPU。在 CPU 上,Float16Array 会减少内存占用和缓存压力,但不会提升算术运算速度。
我该如何处理不支持 Float16Array 的环境?
检查 typeof Float16Array !== 'undefined' 是否成立,若不支持则回退到一个将数据存储在 Uint16Array 中并通过 Float32 转换来模拟操作的 polyfill。Core-js 提供了符合规范的 polyfill。在使用 polyfill 的环境中,性能会变慢,因为每次操作都需要格式转换。
Float16Array 是否会导致在 Float32Array 上工作正常的计算出现错误结果?
是的。精度损失会导致依赖超过 3-4 位小数的算法失效,或在多次迭代中累积舍入误差。坐标系统、迭代求解器和金融计算在使用 Float16 时都会失效。在从 Float32 迁移之前,请先验证精度需求。
当我将超出 ±65504 范围的值赋给 Float16Array 时会发生什么?
超过 ±65504 的值会变成 Infinity 或 -Infinity。幅度小于最小次正规数(约 6×10⁻⁸)的值会被舍入为零。这是一种静默行为,不会报错或发出警告。使用 Float16Array 时应始终验证输入范围。
浏览器支持及生产环境的 Polyfill 策略
Float16Array 已达到 Baseline 2026 状态,这意味着所有主要的常青浏览器在 2025 年底均已支持它。Chrome 125+、Firefox 129+、Safari 18+ 和 Edge 125+ 均已原生实现。Node.js 22.0+ 在核心运行时中包含 Float16Array。
旧环境需要使用 polyfill。core-js 库提供了一种符合规范的 Float16Array 实现,它将值存储在 Uint16Array 中,并在进行算术运算时转换为 Float32。由于每次操作都需要在格式之间往返转换,性能会下降,但正确性得以保留。
生产团队在依赖 Float16Array 之前必须进行特性检测。检查方法很简单:测试全局 Float16Array 是否存在,以及 DataView 是否具备 getFloat16 和 setFloat16 方法。如果任一缺失,则加载 polyfill 或回退到 Float32Array。
function hasNativeFloat16Support (): boolean {
return (
typeof Float16Array !== ' undefined ' &&
typeof DataView !== ' undefined ' &&
' getFloat16 ' in DataView . prototype &&
' setFloat16 ' in DataView . prototype
);
}
// Conditional import (dynamic import for polyfill)
let Float16ArrayImpl : typeof Float16Array ;
if ( hasNativeFloat16Support ()) {
Float16ArrayImpl = Float16Array ;
} else {
// Load polyfill
await import ( ' core-js/proposals/float16array ' );
Float16ArrayImpl = Float16Array ;
}
打包工具可借助如 @babel/preset-env 之类的工具,自动为特定浏览器版本注入 polyfill。面向已知支持 Float16Array 的环境交付的团队可以跳过 polyfill,从而降低包体积。
缺少支持时的失败模式是在构造 new Float16Array() 时出现运行时错误。特性检测可以防止崩溃,但团队需要决定回退策略。将其升级为 Float32Array 会使内存使用翻倍。阻止不支持的用户访问相当于拒绝服务。降级功能(跳过高级特性)通常是最合适的选择。
监控真实世界的采用情况有助于制定支持决策。遥测数据显示 99% 以上的用户使用支持的浏览器时,可以证明删除 polyfill 是合理的。相反,如果 10% 的流量来自较旧的 Android WebView 或嵌入式浏览器,则 polyfill 仍然必要。
以上即为 JavaScript 中 Float16Array 的基本使用模式。将数据格式匹配到硬件精度需求,验证精度损失不会破坏正确性,并在投入生产前进行特性检测。将这些模式应用于 WebGPU 机器学习流水线和 HDR 图形中,性能提升将立即可见。