奇技淫巧学 V8 之六,字符串在 V8 内的表达

2,590 阅读3分钟

先划重点:字符串的表达方式大多与其构造方式有关。

在讲述字符串的奇技淫巧之前,我们需要先了解 V8 对于 JavaScript String 的表达。

在 V8 中字符串有如下 5 种表达模式:

  • SeqString
    • 在 V8 堆内使用(类似数组的)连续空间存储字符串。
    • 实际数据存储时分为 OneByte、TwoByte(Unicode)两类。
  • ConsString(first, second)
    • 在字符串拼接时,采用树形结构表达拼接后(first + second)的字符串。
  • SliceString(parent, offset)
    • 在字符串切割时,采用 offset 与 [length] 表达父字符串(parent)的一部分。
  • ThinString(actual)
    • 直接引用另一个字符串对象(actual)。
    • 在多数情况下可以被认为与 ConsString(actual, empty_string) 等价。
  • ExternalString
    • 代表了产生在 V8 堆外的字符串资源。
    • 实际数据表达时分为 OneByte、TwoByte(Unicode)两类。
ThinString 需要开启 flag: --thin_strings

所有的表达模式均有 [length] 属性(由基类 String 定义)记录了字符串中的字符数,但只有 SeqString 真正存储了字符数据,而 ConsString 、SliceString 、ThinString 均为其它表达模式的引用(不存储字符数据)。

在实际应用过程中,由于经常发生字符串拼接操作,故 ConsString 经常会被用来表达相应的字符串。

为了便于理解,我们来看个例子:

1、创建 2 个英文字符串并拼接它们:

var hello = "hello";
var world = "world";
var hello_world = hello + world;

由于 hello 与 world 中所有字符均为单字节字符,故 V8 采用 SeqOneByteString 来表达它们。

为了提升短字符串拼接后的访问性能,V8 在这里(hello_world)并不会生成 ConsString 而是直接将 hello 与 world 中的字符拷贝到新建的 SeqOneByteString 中来存储它们。

V8 定义最小 ConsString 长度为 kMinLength = 13 个字符。

2、创建 1 个中文字符串并与步骤 1 生成的字符串(hello_world)拼接:

var chinese = "你好世界";
var mixed = hello_world + chinese;

由于 chinese 中存在有双字节字符,故 V8 采用 SeqTwoByteString 来表达它。

拼接后字符串(mixed)超过了最小 ConsString 长度限制,

生成 mixed = ConsString(hello_world, chinese) 这样的树形结构。

3、将步骤 2 生成字符串(mixed)进行切割:

var sliced = mixed.slice(1);

由于切割后字符串(sliced)超过了最小 SliceString 长度限制,

生成 sliced = SliceString(mixed, 1) 这样的空间结构。

V8 定义最小 SliceString 长度也为 kMinLength = 13 个字符。

在这里我们可以看到,只有 SeqOneByteString 与 SeqTwoByteString 实例存储实际的字符,而 SliceString 与 ConsString 实例内部其实只是其它表达模式的引用(不存储字符数据)。

需要注意的是,在 SliceString 场景中被切割掉的字符并不会被 V8 GC 回收。

也就是说,例子中 SeqOneByteString 实例中存储的 "h" 字符并不会被 V8 GC 回收。

总结一下:

  • 字符串在 V8 中采用何种模式表达取决于此字符串的构造方式(Seq 还是 Cons 等)。
  • 字符串拼接时,比起重新分配一个空间进行存储,采用树形结构(ConsString)表达更加高效

了解完字符串在 V8 内的表达,接下来就可以讲讲关于它们的奇技淫巧:

  • 奇技淫巧学 V8 之七,字符串的扁平化(明日发布)