JavaScript内核与高级编程之:`JavaScript`的`Typed Arrays`:其在科学计算和`WebGPU`中的应用。

各位靓仔靓女们,早上好/下午好/晚上好!我是你们的老朋友,今天咱们来聊聊JavaScript里一个有点“硬核”但又非常实用的家伙——Typed Arrays。

这玩意儿,说实话,一开始看到名字,我也有点懵圈。啥叫“类型化的数组”?听起来就像是数组穿了西装,戴了领带,瞬间变身精英人士。但实际上,它比这有趣多了。

一、Typed Arrays:告别JavaScript数组的“任性”

在JavaScript的世界里,普通的数组那是相当的“任性”。它可以放任何类型的数据,数字、字符串、对象,甚至是函数,一股脑儿塞进去都没问题。这很灵活,但对于某些对性能要求极高的场景,比如科学计算、图像处理、WebGPU等等,就显得效率不高了。

为什么呢?

  1. 类型不确定: JavaScript引擎需要时刻检查数组元素的类型,这会增加运行时的开销。
  2. 存储不紧凑: JavaScript数组在内存中不一定是连续存储的,这会导致访问速度变慢。

想象一下,你参加一个宴会,主办方说:“大家随便吃,想吃啥拿啥!” 结果你发现,甜点、主食、饮料混在一起,你需要不停地分辨哪个是你要的,效率自然就低了。

Typed Arrays就像是把宴会分成了不同的区域:甜点区、主食区、饮料区。每个区域只放特定类型的东西,这样你就可以直接去对应的区域拿你想要的,效率大大提升。

二、Typed Arrays家族成员:八大金刚闪亮登场

Typed Arrays并不是一个具体的类型,而是一系列构造函数的集合,每个构造函数对应一种特定的数据类型。 它们就像是八大金刚,各有神通:

构造函数 描述 存储大小(字节) 范围(近似)
Int8Array 8位有符号整数数组 1 -128 到 127
Uint8Array 8位无符号整数数组 1 0 到 255
Uint8ClampedArray 8位无符号整数数组,值会被截取到0-255之间 1 0 到 255
Int16Array 16位有符号整数数组 2 -32768 到 32767
Uint16Array 16位无符号整数数组 2 0 到 65535
Int32Array 32位有符号整数数组 4 -2147483648 到 2147483647
Uint32Array 32位无符号整数数组 4 0 到 4294967295
Float32Array 32位浮点数数组(单精度) 4 大约 ±1.18e-38 到 ±3.4e+38
Float64Array 64位浮点数数组(双精度) 8 大约 ±2.23e-308 到 ±1.80e+308
BigInt64Array 64位有符号大整数数组 8 -263 到 263-1
BigUint64Array 64位无符号大整数数组 8 0 到 264-1

代码示例:创建和使用Typed Arrays

// 创建一个 Int8Array,长度为5
const int8Array = new Int8Array(5);

// 设置数组元素的值
int8Array[0] = -10;
int8Array[1] = 20;
int8Array[2] = 127;
int8Array[3] = -128;
int8Array[4] = 128; // 注意:128超出了 Int8Array 的范围,会被截断为 -128

console.log(int8Array); // Int8Array(5) [ -10, 20, 127, -128, -128 ]

// 创建一个 Float32Array,长度为3
const float32Array = new Float32Array([1.0, 2.5, 3.75]);

console.log(float32Array); // Float32Array(3) [ 1, 2.5, 3.75 ]

// 创建一个 Uint8ClampedArray
const clampedArray = new Uint8ClampedArray([256, -1, 128]);
console.log(clampedArray); // Uint8ClampedArray(3) [255, 0, 128]  超过255会被clamp到255,小于0会被clamp到0

// 获取数组的长度
console.log(int8Array.length); // 5

// 获取数组的元素大小(以字节为单位)
console.log(int8Array.BYTES_PER_ELEMENT); // 1
console.log(float32Array.BYTES_PER_ELEMENT); // 4

三、ArrayBuffer:Typed Arrays的“幕后大哥”

Typed Arrays 并不是直接存储数据的,它们只是对 ArrayBuffer 的一种视图。ArrayBuffer 才是真正存储二进制数据的对象。你可以把 ArrayBuffer 想象成一块原始的内存区域,而 Typed Arrays 则是对这块区域的不同解释方式。

代码示例:ArrayBuffer和Typed Arrays的配合

// 创建一个 ArrayBuffer,大小为16字节
const buffer = new ArrayBuffer(16);

// 创建一个 Int32Array,它指向 ArrayBuffer 的前4个字节
const int32View = new Int32Array(buffer, 0, 1); // (buffer, byteOffset, length)
//创建一个 Float32Array,它指向 ArrayBuffer 的5-8个字节
const float32View = new Float32Array(buffer, 4, 1);

// 通过 Int32Array 修改 ArrayBuffer 的值
int32View[0] = 12345;

// 通过 Float32Array 修改 ArrayBuffer 的值
float32View[0] = 3.14159;

// 创建一个 DataView,它可以访问 ArrayBuffer 的任意字节
const dataView = new DataView(buffer);

// 读取 ArrayBuffer 的第一个字节
const firstByte = dataView.getInt8(0);

console.log(firstByte); // 输出:57

console.log(int32View); // Int32Array(1) [ 12345 ]
console.log(float32View); // Float32Array(1) [ 3.141590118408203 ]

从上面的例子可以看出,ArrayBuffer 就像一个“容器”,Typed Arrays 就像是“刻度尺”,你可以用不同的刻度尺去测量同一个容器,得到不同的结果。

四、DataView:灵活访问ArrayBuffer的“瑞士军刀”

DataView 提供了更灵活的方式来访问 ArrayBuffer 中的数据。你可以指定读取或写入数据的类型和字节序(大端或小端)。

代码示例:DataView的使用

const buffer = new ArrayBuffer(8);
const dataView = new DataView(buffer);

// 设置 ArrayBuffer 的前4个字节为一个32位整数(大端字节序)
dataView.setInt32(0, 0x12345678, false); // (byteOffset, value, littleEndian)

// 读取 ArrayBuffer 的前4个字节为一个32位整数(大端字节序)
const value = dataView.getInt32(0, false);

console.log(value); // 输出:305419896 (0x12345678)

// 设置 ArrayBuffer 的后4个字节为一个32位浮点数
dataView.setFloat32(4, 3.14159, true); // 小端字节序

// 读取 ArrayBuffer 的后4个字节为一个32位浮点数
const floatValue = dataView.getFloat32(4, true);

console.log(floatValue); // 输出:3.141590118408203

DataView 就像一把瑞士军刀,可以让你精细地操作 ArrayBuffer 中的每一个字节。

五、Typed Arrays在科学计算中的应用

科学计算通常需要处理大量的数据,对性能要求很高。Typed Arrays 可以提供更高效的数据存储和访问方式,从而提升计算速度。

场景:向量加法

假设我们需要对两个包含大量元素的向量进行加法运算。

普通JavaScript数组:

function vectorAdd(a, b) {
  const result = [];
  for (let i = 0; i < a.length; i++) {
    result[i] = a[i] + b[i];
  }
  return result;
}

const a = new Array(1000000).fill(1);
const b = new Array(1000000).fill(2);

console.time('Array Add');
const sum = vectorAdd(a, b);
console.timeEnd('Array Add'); // 约 10-20ms (取决于机器)

Typed Arrays:

function typedVectorAdd(a, b) {
  const result = new Float32Array(a.length);
  for (let i = 0; i < a.length; i++) {
    result[i] = a[i] + b[i];
  }
  return result;
}

const a = new Float32Array(1000000).fill(1);
const b = new Float32Array(1000000).fill(2);

console.time('TypedArray Add');
const typedSum = typedVectorAdd(a, b);
console.timeEnd('TypedArray Add'); // 约 2-5ms (取决于机器)

可以看到,使用 Typed Arrays 后,计算速度明显提升。这是因为 Typed Arrays 在内存中是连续存储的,而且避免了 JavaScript 引擎的类型检查开销。

六、Typed Arrays在WebGPU中的应用

WebGPU 是一种用于在 Web 平台上进行高性能图形和计算的 API。它允许 JavaScript 代码直接访问 GPU 的底层功能,从而实现更强大的图形渲染和并行计算能力。

Typed Arrays 在 WebGPU 中扮演着重要的角色。WebGPU 使用 Typed Arrays 来传递数据给 GPU,例如:

  • 顶点数据: 描述 3D 模型的顶点位置、颜色、法线等信息。
  • 索引数据: 定义 3D 模型的三角形连接方式。
  • 纹理数据: 存储图像数据,用于渲染 3D 模型的表面。
  • Uniform 数据: 存储着色器程序需要的常量值,例如光照参数、变换矩阵等。

WebGPU要求输入的数据必须是特定的Typed Arrays,这样才能保证数据格式的正确性和效率。

代码示例(简化):WebGPU中使用Typed Arrays

// 创建一个 ArrayBuffer,用于存储顶点数据
const vertexBufferData = new Float32Array([
  -0.5, -0.5, 0.0,  // 顶点1的 x, y, z 坐标
  0.5, -0.5, 0.0,   // 顶点2的 x, y, z 坐标
  0.0, 0.5, 0.0     // 顶点3的 x, y, z 坐标
]);

// 创建一个 GPUBuffer,并将顶点数据复制到 GPU 内存中
const vertexBuffer = device.createBuffer({
  size: vertexBufferData.byteLength,
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
  mappedAtCreation: true, //buffer创建时就map,允许立即写入
});

new Float32Array(vertexBuffer.getMappedRange()).set(vertexBufferData);
vertexBuffer.unmap();

// 在 WebGPU 渲染管线中,将 vertexBuffer 绑定到顶点着色器的输入
const vertexBufferLayout = {
  arrayStride: 12, // 每个顶点占用 12 个字节 (3 个 float32)
  attributes: [{
    shaderLocation: 0, // 顶点着色器中 attribute 的位置
    offset: 0,         // 从顶点数据的起始位置开始
    format: 'float32x3'   // 数据格式为 float32 向量 (3 个分量)
  }]
};

在这个例子中,我们使用 Float32Array 来存储顶点数据,并将它传递给 WebGPU 的 GPUBuffer。WebGPU 通过 vertexBufferLayout 来了解顶点数据的格式和布局。

七、Typed Arrays的注意事项

  1. 类型固定: 一旦创建了 Typed Array,它的类型就不能改变了。如果你需要存储不同类型的数据,需要创建多个 Typed Array。
  2. 长度固定: Typed Array 的长度也是固定的,不能像普通数组那样动态地添加或删除元素。如果需要动态调整大小,需要创建新的 Typed Array 并复制数据。
  3. 不是真正的数组: Typed Arrays 并不是真正的 JavaScript 数组,它们没有 pushpop 等方法。但它们可以使用数组的下标访问元素,并且具有 length 属性。
  4. 拷贝问题: Typed Arrays的拷贝需要注意深拷贝和浅拷贝的问题。slice() 方法返回的是一个新的 Typed Array,但是它共享底层的 ArrayBuffer。如果需要完全独立的拷贝,需要手动创建新的 ArrayBuffer 并复制数据。

八、总结

Typed Arrays 是 JavaScript 中用于处理二进制数据的利器。它们可以提供更高效的数据存储和访问方式,从而提升性能。在科学计算、图像处理、WebGPU 等对性能要求极高的场景中,Typed Arrays 发挥着重要的作用。

虽然 Typed Arrays 的学习曲线可能稍微陡峭,但是一旦掌握了它的基本概念和用法,你就可以在 JavaScript 中编写出更高效、更强大的代码。

好了,今天的讲座就到这里。希望大家能够对 Typed Arrays 有更深入的了解。记住,掌握了 Typed Arrays,你就能让你的 JavaScript 代码“硬”起来! 下次有机会再和大家分享更多好玩的技术。拜拜!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注