《WebAssembly 权威指南》(4):WebAssembly 内存

1e16c4786b6587d5399a43669b97405f.gif

本文是《WebAssembly 权威指南》系列文章第 4 篇,系列文章列表:


WebAssembly 就像一个运行时环境,它需要一种方法来分配和释放内存以进行数据处理。在本章中,我将向你展示它如何模拟这种行为以提高效率,但不会出现 C 和 C++ 等语言典型的内存操作问题风险(即使我们正在运行它们)。由于我们有可能在 Internet 上下载任意代码,因此这是一个重要的安全考虑因素。

计算的整个概念通常涉及某种形式的数据处理。无论我们是在对文档进行拼写检查、处理图像、进行机器学习、对蛋白质进行测序、玩视频游戏、看电影,还是只是在电子表格中处理数字,我们都经常与任意数据块进行交互。这些系统中最关键的性能考虑因素之一是如何将数据获取到需要的位置,以便可以以某种方式对其进行查询或转换。

当数据在寄存器或高速缓存中可用时,中央处理器 (CPU) 工作得最快。显然,这些都是非常小的容器,因此大型数据集永远不会完全加载到 CPU 上。我们必须花费一些精力将数据移入和移出内存。等待数据加载到这些位置之一的成本是 CPU 时钟时间。这就是它们变得如此复杂的原因之一。现代芯片具有各种形式的多线程、预测分支和指令重写,当我们从网络读取到主内存,从主内存到多级缓存,最后到我们需要使用它的地方时,这些芯片可以保持 CPU 处于忙碌状态。

传统程序通常有堆栈内存来管理小的或固定大小的短期变量。它们使用基于堆的内存来管理长期存在的、任意大小的数据块。这些通常只是分配给程序的不同内存区域,它们的处理方式不同。堆栈内存经常被执行期间调用的函数覆盖。堆内存在不再需要时被使用和清理。如果一个程序用完了内存,它可以请求更多的内存,但它必须对如何使用它做出合理的决定。今天,虚拟分页系统和更便宜的内存使一台典型的计算机拥有数十 GB 以上的内存成为可能。能够快速有效地访问潜在大数据集的单个字节是软件运行时性能的关键。

TypedArray

传统上,JavaScript 不提供对内存中单个字节的轻松访问。这就是为什么时间敏感的低级功能通常由浏览器或某种插件提供。即使是 Node.js 应用程序也经常使用比 JavaScript 更好地处理内存操作的语言来实现某些功能。这使事情变得复杂,因为 JavaScript 是一种解释型语言,你需要一种有效的机制来在解释型、可移植代码和快速编译代码之间来回切换控制流。它还使部署变得更加棘手,因为应用程序的一部分本质上是可移植的,而另一部分则需要不同操作系统上的本机库支持。

在软件开发中通常需要权衡:语言要么快速要么安全。当你需要原始速度时,你可能会选择 C 或 C++,因为它们在内存中使用和操作数据时几乎不提供运行时检查。因此,它们非常快。当你需要安全时,你可以选择一种对数组引用进行运行时边界检查的语言。速度权衡的缺点是事情要么很慢,要么内存管理的负担落在程序员身上。不幸的是,忘记分配空间、重用释放的内存或在完成后未能释放空间是非常容易出错的。这就导致了用这些快速语言编写的应用程序经常出现错误、容易崩溃,并且是许多安全漏洞的来源之一。

Java 和 JavaScript 等具有垃圾回收功能的语言将开发人员从管理内存的负担中解放出来,但作为交换,运行时的性能往往会受到影响。运行时的一部分必须不断地找到未使用的内存并释放它。性能开销使得许多此类应用程序不可预测,因此不适合嵌入式应用程序、金融系统或其他对时间敏感的用例。

只要创建的内存大小适合你要放入的内容,分配内存就不是什么大问题。棘手的部分是何时清理。显然,在程序完成之前释放内存是不合适的,但是当不再需要它时不这样做就变得低效,你可能会耗尽内存。像 Rust 这样的语言在便利性和安全性之间取得了很好的平衡。编译器会强制你更清楚地表达你的意图,但当你这样做时,它会更有效地为你清理。

如何在运行时对其进行管理通常是语言及其运行时的定义特征之一。因此,并非每种语言都需要相同级别的支持。这就是为什么 WebAssembly 的设计者没有在 MVP 中过度指定垃圾收集等功能的原因之一。

JavaScript 是一种灵活且动态的语言,但它在历史上并未使处理大型数据集的单个字节变得简单或高效。这使底层库的使用变得复杂,因为必须将数据复制为 JavaScript 的本机格式,效率不高。使用 Array 类存储 JavaScript 对象,这意味着它必须准备好处理任意类型。Python 的许多容器也是灵活和臃肿的。通过指针快速遍历和操作内存是连续块中数据类型统一的产物。字节是最小的可寻址单位,尤其是在处理图像、视频和声音文件时。

处理数值数据需要更多的努力。一个 16 位整数占用两个字节。一个 32 位整数需要四个字节。字节数组中的位置 0 可能表示数据数组中的第一个这样的数字,但第二个数字将从位置 4 开始。

JavaScript 添加了 TypedArray 接口来解决这些问题,最初是为了提高 WebGL 性能。这些是内存的一部分,可通过 ArrayBuffer 实例访问,可以将其视为特定数据类型的同类块。可用内存受 ArrayBuffer 实例限制,但它可以以方便传递到本机库的内部格式存储。

在例 4-1 中,我们看到了创建 32 位无符号整数类型数组的基本功能。

例 4-1. 在 Uint32Array 中创建十个 32 位整数

var u32arr = new Uint32Array (10);
u32arr [0] = 257;
console.log (u32arr);
console.log ("u32arr length:" + u32arr.length);

输出如下:

Uint32Array (10) [257, 0, 0, 0, 0, 0, 0, 0, 0, 0]
u32arr length: 10

如你所见,对于整数数组,这正如你所期望的那样工作。请记住,这些是 4 字节整数(因此类型名称中的 32)。在例 4-2 中,我们从 Uint32Array 获取底层的 ArrayBuffer 并将其打印出来。这表明它的长度是 40。接下来,我们用表示无符号字节数组的 Uint8Array 包装缓冲区,并打印出它的内容和长度。

例 4-2. 访问 32 位整数作为 8 位字节的缓冲区

var u32buf = u32arr.buffer;
var u8arr = new Uint8Array (u32buf);
console.log (u8arr);
console.log ("u8arr length:" + u8arr.length);

输出如下:

ArrayBuffer {byteLength: 40}
Uint8Array (40) [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, ...]
u8arr length: 40

ArrayBuffer 表示原始底层字节。TypedArray 是这些字节基于指定类型大小的相互预设视图。所以当我们初始化 Uint32Array 的长度为 10 时,这意味着 10 个 32 位整数,需要 40 个字节来表示。分离缓冲区非常大,可以容纳所有 10 个整数。由于 Uint8Array 的大小定义,它将每个字节视为一个单独的元素。

看看图 4-1,你就会明白发生了什么。Uint32Array 的第一个元素(位置 0)只是值 257。这是 ArrayBuffer 中底层字节的解释视图。Uint8Array 直接反映缓冲区的底层字节。图底部的位模式反映了前两个字节中每个字节的位。

1611273b02991ea4287f1b0b1a4d264f.png

图 4-1. 代表值 257
<
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值