前言
本文重点介绍 字符串 在 V8 内的内存布局和使用
字符串的编码
v8 使用两种方式编码 String
- oneByteString
- twoByteString
当字符串内的所有字符都可以用 Latin1 编码表示时, 使用 oneByteString 的形式编码, 每个字符对应一个字节, 底层是 uint8_t[], 为了节约空间.
Latin1 编码的表达范围在 0-255, 可以用一个字节表示
如果字符串中存在非 Latin1 编码能表示的字符, 那么就使用 UTF-16 编码, 固定每个字符2 个字节, 也就是 twoByte 的含义
举个例子
let str = 'abcd'
str 被存储在单位为 byte 的数组, 4 个字符大小为 4Byte
如果字符串存在非 Latin1 编码的字符
let str = 'abcd字'
str 底层为uint16_t[], 每个字符 2 字节, 对应大小是 5*2Byte = 10Byte
对应的源代码
v8 在 parse js 代码 时确定某个字符串是 oneByte 还是 twoByte
在 parse 阶段, 会依次处理 a b c d 字 每个字符
abcd都可以用一个字节表示, 所以一开始用 uint8_t[] 存储, 调用 AddOneByteChar 添加下一个字符
当发现了字符 字, 就会将前面的所有字符重新拷贝一份, 重新存储为 uint16_t[] 的结构, 每个字符用 2 个字节存储
V8 String 的生命周期
以下面这段代码为例
let str = 'abcdef'
String 的创建
v8 在 parse 阶段对 abcdef 进行 hash 计算, 放入 string_table_ 里面, 用于去重, 避免重复分配 string 内存, 返回AstRawString
AstRawString 表示 parse ast 中产生的所有 string片段, 里面包含了 str 和 abcdef
然后将所有 parse 阶段发现的 AstRawString 拷贝到 isolate 里面
上一篇文章说过了, 所有 js 对象都存储在
isolate内
入口在 PostProcessParseResult
这个过程一定会发生拷贝, isolate 会重新拷贝一遍 string 内存, 实际调用 isolate->factory()->InternalizeStringWithKey(&key),
这个过程也会根据 hash 去重, 重复的字符串在内存里不会存两次
之后 AstRawString 被替换成 InternalizeString
创建 String 对象的过程
创建 string 对象有两个比较重要的概念,一个是内存分配 allocate,一个是 Map
内存分配 allocate
string分配的内存大小要求 4 字节对齐, 所有 js 对象在 v8 中分配都是需要对齐的
对齐后实际分配的内存如下
// oneByteString
allocate_size = (14 + str_length) + 3 & ~3
14 字节是 SeqOneByteString 对象本身的大小, v8 用该对象表示连续的 oneByteString
例如 abcdef 长度是 6, 实际分配的内存大小等于 20Byte
Map
Map 里面描述了当前的 js 对象的 shape(形状)
所有 js 对象在 V8 内都存有一个 Map 用来描述对象的结构
比如访问一个对象的属性,首先要通过 Map 找到对应属性字段的位置,然后再访问
比如判断 Handle<String> 的类型是 SeqOneByteString 还是 SeqTwoByteString, 需要去 Map 里面去查找
ConsString(拼接 String)
当两个字符串拼接的时候
let a = "abc";
let b = "def";
let c = a + b;
拼接操作的判断还是挺多的,因为要考虑左右两边的类型情况,抛开类型判断分支的逻辑不说
如果a.length + b.length 小于 kMinLength, 那么就将两个字符串都拷贝一遍,生成一个新的字符串
// Minimum length for a cons string.
static const uint32_t kMinLength = 13;
如果大于kMinLength, 那么就使用 ConsString,ConsString 将两个字符串的指针分别存放在 first和 second 字段,这样就不会发生拷贝
下面是示意图
可以用 d8 验证一下
d8 是 V8 引擎的命令行调试工具,可以查看 js 代码对应的字节码
let a = '123456';
let b = '123456';
let c = a + b; // c.length 小于 13
%DebugPrint(c);
v8-debug --allow-natives-syntax ./example.js
输出
DebugPrint: 0x5cd0028852d: [String]: "123456123456"
0x5cd000000b5: [Map] in ReadOnlySpace
- map: 0x05cd00000475 <MetaMap (0x05cd0000002d <null>)>
- type: SEQ_ONE_BYTE_STRING_TYPE
重点是SEQ_ONE_BYTE_STRING, 说明它是一个整体连续的字符串
把代码稍微改一下
let a = '123456';
let b = '1234567';
let c = a + b; // c.length 超过了 13
%DebugPrint(c);
输出结果
DebugPrint: 0x399700288531: [String]: c"1234561234567"
0x399700000385: [Map] in ReadOnlySpace
- map: 0x399700000475 <MetaMap (0x39970000002d <null>)>
- type: CONS_ONE_BYTE_STRING_TYPE
结果是 CONS_ONE_BYTE_STRING, 这个过程没有发生拷贝
常量折叠
如果是两个字符串字面量拼接
let str = "abc" + "def";
上面的代码在 parse ast 阶段会发生常量折叠
如果出现1 + 2 这样的表达式,v8 在 parse 阶段就会直接计算出结果
如果出现两个字符串相加,v8 会生成AstConsString, 内部用两个指针将两个 String 拼接起来,这个过程并不会发生内存拷贝
但是常量池会被整理,最后所有 ConsString都会被 flat, 所以 str 还是连续的,而不是 ConsString
SlicedString 字符串的切片
SlicedString 也是一种为了避免内存拷贝的数据结构
let a = "fdafdafds";
let sliced_str = a.slice(1);
String.prototype.slice 和 String.prototype.trim 等函数都有可能返回 SlicedString
源代码在 NewSubString
这里会有一个逻辑判断,如果结果的长度小于SlicedString::kMinLength, 那么就完整 copy 一份出来
SlicedString::kMinLength 的长度也是 13
如果大于等于 13,就会使用偏移量和 length 的形式表示
let str = "a".repeat(14);
let sliced_str = str.slice(2) // length: 12; 这里会发生完成的拷贝
let sliced_str = str.slice(1) // length: 13; 这里不会发生拷贝,内部使用偏移量 + length 表示
大致结构如下,使用 parent 指向 str, 内部存一个 offset 和 length,
ThinString
let str1 = "abcdefg";
let str2 = "abcdefg"
let str = str1 + str2; // ConsString
运行上面的代码时 StringTable 会存储所有的 变量名 和 abcdefg 作为内部字符串
当 str 作为 Object 的 key 时
const object = {}
object[str] = 0
str 会被放入 StringTable,便于后面快速查找,在这个过程中,str 实际内容转换成了 内部字符串
str 会原地留下一个指针, 也就是 ThinString , v8 给的解释是,因为无法原地内部化,所以才会搞出这个设计
如何查找 V8 String 相关函数的定义
比如你想查看 String.prototype.at
v8 把内部方法调用放在 src/buildtins 下面
需要去 src/builtins 目录下面查找,你会发现如下的代码,这个是用 Torque 实现的
内部会调用 args.receiver() 得到 Handle<Object>
然后将 Handle<Object> 重新解释成为 Handle<String>
最后一行,用了两个函数StringCharCodeAt和StringFromSingleCharCode
具体代码实现在CodeStubAssembler::StringCharCodeAt和 CodeStubAssembler::StringCharCodeAt, 具体代码我就不贴了
可以自行搜索
结尾
本文主要介绍了 V8 String 的创建和一些特殊的 String 结构, 算是对V8 String 的入门介绍
因为个人能力有限,如果有错误请在评论区指出