将 Base64 编码的数据快速转换为 Uint8Array

为了方便节省请求数,有时我们会将二进制数据以 Base64 编码的形式嵌入 HTML 或 JavaScript 中,再于运行时解码成 Uint8Array,进行后续运算。然而浏览器没有提供直接的 API 来完成这种解码转换,需要我们自己实现。本文介绍两种快速的解码方法。

数据准备

const data = Array.from({ length: 100 * 1024 }, () => Math.floor(Math.random() * 256))
const raw = String.fromCharCode(...data)
const b64data = btoa(raw)

为测试不同解码方法的性能,我们生成了一组 100KB 的随机数据,并将其转换为 Base64 编码的字符串 b64datab64data 将作为解码函数的输入,以测试不同解码方法的性能。

方法一:循环拷贝

function decode1(b64data) {
const raw = atob(b64data)
const buf = new Uint8Array(raw.length)
for (let i = 0; i < raw.length; i++) buf[i] = raw.charCodeAt(i)

return buf
}

方法一最为直接。首先使用 atob 函数将 b64data 解码为包含原始数据的字符串 raw,再通过一个循环拷贝 raw 的各个字符点位到字节数组 buf 中。这种方式的计算密集部分都是由 JS 完成的,尽管 V8 引擎有 JIT,运行性能仍是不理想:

console.time("decode1")
decode1(b64data)
console.timeEnd("decode1") // decode1: 1.2431640625 ms

方法二:使用 TextEncoder

注意 该方法解码的结果和原始数据是不等价的,请阅读下面分析后再决定是否使用。

function decode2(b64data) {
const raw = atob(b64data)
const buf = new Uint8Array(raw.length * 2)
const { written } = new TextEncoder().encodeInto(raw, buf)
return buf.subarray(0, written)
}

有没有其他捷径能取代方法一中的热循环呢?答案是使用 TextEncoder.encodeInto(str, buf) 函数。这个函数会将字符串 str 编码为 UTF-8 并写入到字节数组 buf 中。这一切都是在底层的 C++ 代码中完成的,因此性能会更好。以相同方式测试,可以观察到 decode2 的耗时仅有 decode1 的 60%。

console.time("decode2")
decode2(b64data)
console.timeEnd("decode2") // decode2: 0.7060546875 ms

decode2 的弊端和应用场景

读者可能会奇怪,为什么 decode2 中将 buf 的长度初始化为 raw.length * 2 而非 raw.length 呢?这是因为 TextEncoder.encodeInto 会将字符串编码为 UTF-8,我们得到的其实是原始数据的 UTF-8 表示。由于 b64data 编码自字符串 raw,而 raw 的每个字符范围为 [0, 255],在 UTF-8 编码后,字符范围为 [128, 255] 的部分会变成两个字节。因此,我们需要将 buf 的长度初始化为 raw.length * 2 作为最大字节数上限。TextEncoder.encodeInto 返回的 written 字段会告诉我们实际写入的字节数,我们可以通过 buf.subarray(0, written) 来截取有效部分。

可惜,TextEncoder 默认只能编码 UTF-8。如果它支持 Latin-1 编码,decode2 的解码结果便会和原始数据完全等价。那既然 decode2 的结果和原始数据不等价,那它岂不是无用?也不尽然。

  • 如果原始数据的每个字节范围都是 [0, 127],那么 decode2 的结果和原始数据是等价的。这是因为 ASCII 字符在 UTF-8 编码中仍然是单字节的。因此,如果你的数据确保了这一点,那么 decode2 会是一个更快的解码方法。

  • 如果数据的下游能接受这种“异化”后的解码结果,decode2 也可被使用。一个例子是解码后的数据将作为 WebAssembly 模块的输入。我们可以在编写 WASM 代码时提前考虑到这一点,实现一个简单的“双字节 UTF-8 -> 单字节”的转换函数,即可享受 decode2 的性能,并同时兼顾数据的正确性。


作者:hsfzxjy
链接:
许可:CC BY-NC-ND 4.0.
著作权归作者所有。本文不允许被用作商业用途,非商业转载请注明出处。

«辩义 State、Nation 与 Country

OOPS!

A comment box should be right here...But it was gone due to network issues :-(If you want to leave comments, make sure you have access to disqus.com.