类型化数组与字符串的内存开销

Memory overhead of typed arrays vs strings

我正在尝试减少 javascript Web 应用程序的内存使用量,该应用程序以大量小字符串的形式在内存中存储大量信息。当我将代码更改为使用 Uint8Array 而不是 String 时,我注意到内存使用量上升了。

例如,考虑以下创建许多小字符串的代码:

// (1000000 strings) x (10 characters)
var a=[];
for (let i=0; i<1000000; i++)
    a.push("a".repeat(10).toUpperCase());

如果您将它放在一个空页面中并让内存使用稳定几秒钟,它会稳定在 70 MiB Google Chrome .另一方面,以下代码:

// (1000000 arrays) x (10 bytes)
var a=[];
for (let i=0; i<1000000; i++)
    a.push(new Uint8Array(10));

使用 233 MiB 内存。没有任何代码的空白页面使用大约 20 MiB。另一方面,如果我创建少量大 strings/arrays,差异会变小,并且在单个 string/array 和 10000000 characters/entries 的情况下,内存使用几乎相同。

那么为什么类型化数组有如此大的内存开销?

这里是 V8 开发人员。您的结论是有道理的:如果将字符串中的字符与 Uint8Array 中的元素进行比较,则字符串的开销会更少。 TypedArrays 非常擅长提供对类型化元素的快速访问;然而,拥有大量小型 TypedArrays 的内存效率不高。

区别在于字符串和类型数组的 object header 大小。

对于字符串,object header 是:

  1. 隐藏class指针
  2. 散列
  3. 长度
  4. 有效载荷

有效载荷向上舍入为指针大小对齐,因此在本例中为 16 个字节。

对于 Uint8Array,您需要以下内容:

  1. 隐藏class指针
  2. 属性指针(未使用)
  3. 元素指针(见下文)
  4. 数组缓冲区指针(见下文)
  5. 数组缓冲区的偏移量
  6. 字节长度
  7. 数组缓冲区的视图长度
  8. 长度 (user-visible)
  9. 嵌入字段 #1
  10. 嵌入字段 #2

  11. 数组缓冲区:隐藏class指针

  12. 数组缓冲区:属性指针(未使用)
  13. 数组缓冲区:元素指针(见下文)
  14. 数组缓冲区:字节长度
  15. 数组缓冲区:后备存储
  16. 数组缓冲区:分配基数
  17. 数组缓冲区:分配长度
  18. 数组缓冲区:位域(内部标志)
  19. 数组缓冲区:嵌入字段#1
  20. 数组缓冲区:嵌入字段#2

  21. 元素object:隐藏class指针

  22. 元素object:长度(后备存储)
  23. 元素object:(后备存储的)基指针
  24. 元素object:数据开始的偏移量
  25. 元素object:有效负载

其中,有效负载再次四舍五入为指针大小对齐,因此此处占用 16 个字节。

综上所述,每个字符串占用 5*8 = 40 个字节,每个类型数组占用 26*8 = 208 个字节。这看起来确实有很多开销;原因是由于 TypedArrays 提供的各种灵活选项(它们可以重叠视图到 ArrayBuffers,可以直接从 JavaScript 分配,或与 WebGL 共享等等)。

(这与 "optimizing memory allocation" 无关,也与 "better at garbage collecting strings" 无关——因为你持有所有 object,GC 不起作用。)

类型化数组不应该以这种方式使用。

如果您想要高内存效率,请只使用一个类型化数组来保存所有整数。而不是因为底层原因而使用大量的数组来保存你的整数。

这些低级原因与将一个对象保存在内存中需要多少开销有关,而该数量取决于几个方面,例如不变性和垃圾收集。在这种情况下,保持一个类型化数组比保持一个简单字符串的开销更高。这就是为什么你应该只支付一次这个价格

你应该利用:

var a = [];                       for (let i=0; i<1000000; i++) a.push("1");
var b = new Uint8Array(10000000); for (let i=0; i<1000000; i++) a[i] = 1;
// 'b' is more memory efficient than 'a', just pay the price of Uint8Array one time
// and save the wasted memory in string allocation overhead