最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 「译」JavaScript 是如何计算 1+1 的 - Part 1 创建源码字符串

    正文概述 掘金(qqqqqcy)   2021-04-07   826

    来源:medium.com/compilers/c…

    「译」JavaScript 是如何计算 1+1 的 - Part 1 创建源码字符串 毫无疑问 1 + 1 = 2,但是 V8 JavaScript 的引擎是如何计算出来的呢?

    题外话,我最喜欢的一个面试问题是:「从输入 URL 到页面加载发生了什么?」 _ 这是一个很好的问题,因为它能展示一个人相关知识的深度和广度,能从回答这个问题的过程中,发现哪些部分是他最感兴趣的

    这是一系列博文中的第一篇,将探讨 V8 在 1 + 1 被输入之后的一切。首先,我们将关注 V8 如何在其堆内存中存储 1 + 1 字符串。这听起来很简单,但它完全值得这一整篇的博文!

    一、客户端应用(The Client Applicant)


    要计算 1 + 1,你可能最先采取的方法是启动 NodeJS,或者打开 Chrome 开发者控制台,然后简单地输入 1 + 1。但为了展示 V8 的内部结构,我决定修改 hello-world.cc,这是 V8 源代码中的一个标准示例应用程序

    我把原来打印 "Hello World" 的代码,用 1 + 1 的表达式代替

    // 创建一个包含 JavaScript 源代码的字符串
    Local<String> source = String::NewFromUtf8Literal(isolate, "1 + 1");
    
    // 编译源代码
    Local<Script> script = 
        Script::Compile(context, source).ToLocalChecked();
    
    // 运行该脚本以获得结果
    Local<Value> result = script->Run(context).ToLocalChecked();
    
    // 将结果转换为Number并打印出来
    Local<Number> number = Local<Number>::Cast(result);
    printf("%f\n", number->Value());
    
    // 导入类 String、Script,导入类型集合 Local
    import { String, Script, Number, Local } from  'v8'
    
    const source: Local["String"] = String.NewFromUtf8Literal(isolate, "1 + 1");
    
    const script: Local["Script"] = Script.Compile(context, source).ToLocalChecked();
    
    const result: Local["Value"] = script.Run(context, source).ToLocalChecked();
    
    const number: Local["Number"] = Number.Cast(result);
    
    console.log(number.Value());
    

    快速阅读这段代码并大概了解一下。这些 C++ 代码看起来难以理解,但注释会应该能帮到你。在这篇博文中,我们主要关注第一句代码,即在 V8 堆中分配一个新的 1 + 1 字符串

    Local<String> source = String::NewFromUtf8Literal(isolate, "1 + 1");
    

    为了理解这段代码,我们先从所涉及的一系列 V8 模块开始。在此图中,执行流程是由左至右,返回值从右至左传回,插入到 soruce 变量中

    「译」JavaScript 是如何计算 1+1 的 - Part 1 创建源码字符串

    • 应用程序 - 这代表了 V8 的客户端,在我们的例子中,它是 hello-world.cc 程序。但通常情况下,它是整个 Chrome 浏览器、NodeJS 运行时系统或任何其他嵌入了 V8 JavaScript 引擎的软件

    • V8 外部 API - 这是一个面向客户端的 API,提供对 V8 功能的访问。虽然它是用 C++ 实现的,但 API 是围绕着各种 JavaScript 概念来塑造的,如数字、字符串、数组、函数和对象,允许以各种方式创建和操作它们

    • 堆工厂 - V8 引擎内部(不通过 API 暴露)是一个在堆上创建各种数据对象的「工厂」。令人惊讶的是,可用的工厂方法集与外部 API 提供的方法有很大的不同,所以很多转换是在 API 层内部完成的

    • New Space - V8 的堆非常复杂,但新分配的对象通常存储在 New Space 中,通常被称为 新生代。我们在这里就不详细介绍了,但是 New Space 是使用 Cheney 算法来管理的,Cheney 算法是一种执行垃圾回收的著名算法

    现在我们来详细了解一下这个流程,重点是:

    • API 层如何决定创建什么类型的字符串,以及它在堆中的存储位置
    • 字符串的内部内存布局是怎样的。这取决于字符串里字符的范围
    • 如何从堆中分配空间。在我们的例子中,需要 20 个字节
    • 最后,如何将指向字符串的指针返回给应用程序,用于未来进行垃圾回收

    二、确定存储字符串的方式和位置


    如上所述,在客户端应用程序堆工厂(实际创建对象的地方)之间必须进行大量的转换工作。大部分的工作都在 src/api/api.cc 中进行

    让我们从客户端应用程序的调用开始:

    String::NewFromUtf8Literal(isolate, "1 + 1");
    

    第一个参数是「Isolate(隔离)」,它是 V8 的主要内部数据结构,代表运行时系统的状态,与其他可能存在的 V8 实例隔离。要理解这一点,可以想象打开了多个浏览器窗口,每个窗口都有一个完全独立的 V8 实例在运行,每个实例都有自己的隔离堆。我们不会多谈 isolate 参数,只需要知道到很多 API 的调用都需要这个参数

    String::NewFromUtf8Literal 方法 (见 src/api/api.cc) 首先进行基本的字符串长度检查,同时也决定如何在内存中存储字符串。 考虑到我们只提供了两个参数,第三个 type 参数默认为NewStringType::kNormal,表示字符串应该作为常规对象在堆上分配。另一种方法是传递NewStringType::kInternalized,表示需要对字符串进行去重复处理。这个特性对于避免存储同一个常量字符串的多个副本非常有用

    内部会接着调用 NewString 方法(见 [src/api/api.cc](https://github.com/v8/v8/blob/8.8.276/src/api/api.cc)),它调用 factory->NewStringFromUtf8(string)。请注意,这里的 string 已经被映射到一个内部的 Vector 数据结构中,而不是一个普通的 C++ 字符串,因为堆工厂有一套与外部 API 完全不同的方法。当返回值传回客户端应用程序时,这种差异将在后面变得更加明显

    NewStringFromUtf8 内部(见 src/heap/factory.cc),决定了字符串的最佳存储格式。当然,UTF-8 是一种方便的格式,可以存储广泛的 Unicode 字符,但是当只使用基本的 ASCII 字符时 (例如 1 + 1) V8 会以 「1 个字节」的格式存储字符串。为了做出这个决定,字符串的字符被传递到 Utf8Decoder decoder(utf8_data) 中(在 src/strings/unicode-decoder.h 中声明)

    现在我们已经决定分配一个 1 字节的字符串,使用普通的(不是内部化的)方法,下一步是调用NewRawOneByteString(见 src/heap/factory-base.cc),在这里,堆内存被分配,字符串的内容被写入该内存

    三、字符串的内存结构


    在 V8 内部,我们的 1 + 1 字符串被表示为 v8::Internal::SeqOneByteString 类的一个实例 (见 src/objects/string.h)。如果你像大多数面向对象的开发者一样,你会期望 SeqOneByteString 有许多公共方法,以及一些私有属性,比如一个字符数组或一个存储字符串长度的整数。然而,事实并非如此! 相反,所有内部对象类实际上只是指向堆中存储这些数据地址的指针

    src/objects/objects.h 中的代码注释可以看出,大约有 150 个内部类的父类是 v8::Internal::Object。这些类中都只包含了一个 8 字节的值(在 64 位机器上),指向了堆中对象所在的地址

    「译」JavaScript 是如何计算 1+1 的 - Part 1 创建源码字符串

    其中有趣的部分是:

    SeqOneByteString 对象

    如前所述,这不是一个功能完善的字符串类,而是一个指向堆中字符串实际内容地址的指针。在 64 位的机器上,这个「指针」将是一个 8 字节的 unsigned long (无符号长整形),其类型别名为 Address。请注意,堆上的数据(在图的右边)实际上并不是一个真正的 C++ 对象,所以没有必要把这个 Address 当作一个指向强类型的东西(如 String *)的指针来处理

    但是,你可能想知道为什么要先有一个间接层,而不直接访问 Heap Block 呢?当你考虑到垃圾收集会导致对象在堆中移动时,会知道这种方法是有意义的。重要的是,数据可以移动,而不会让客户端应用程序感到困惑

    要说明的是,在 Generational Garbage Collection(代际垃圾收集)中,对象首先在 新生代(New Space)中分配,如果它们存活的时间足够长,就会被移到 老生代(Old Space)中。为了实现这一目的,垃圾收集器会将Heap Block 复制到新的堆空间,然后更新 Address 值指向新的内存地址。鉴于 SeqOneByteString 对象本身的内存地址仍然和之前完全相同,客户端软件不会注意到这个变化。

    Compressed Pointer To Map (Heap Block 的第 0-3 个字节)(指向 Map 的压缩指针)

    JavaScript 是一种动态类型的语言,这意味着 _变量 _没有类型,然而 _存储在变量中的值 _却有类型。「map 」是 V8 将堆中的每个对象与其数据类型描述关联起来的方式。毕竟,如果对象没有被标记上它的类型,Heap Block 就会变成一个串无意义的字节

    除了提到 maps 也是存储在 _只读空间 _中的一种堆对象之外,我们不会对 1 + 1 字符串的 map 进行更多的详细介绍。 Maps(也被称为形状或隐藏类)可以变得非常复杂,尽管我们的常量字符串通过调用read_only_roots().one_byte_string_map()(见 src/heap/factory-base.cc)使用了一个预先定义的 map

    有趣的是,虽然这个 map 字段是指向另一个堆对象的指针,但它巧妙地使用了指针压缩,在一个 32 位的字段中存储了一个 64 位的指针值

    Object Hash Value (Heap Block 的第 4-7 个字节)(对象哈希值)

    每个对象都有一个内部的哈希值,但在这个例子中,它默认为 kEmptyHashField(值为3),表示哈希值还没有计算出来

    String Length (Heap Block 的第 8-11 个字节)(字符串长度)

    这是字符串中的字节数(5)(两个 1,两个 ,一个 +

    The Characters and the Padding (Heap Block 的第 12-19 个字节)(字符和填充物)

    正如你所期望的那样,接下来存储的是 5 个单字节字符。此外,为了确保未来的堆对象根据 CPU 的架构要求进行对齐,还额外增加了 3 个字节的填充(将对象对齐到 4 字节的边界)。

    四、从堆中分配内存

    我们简单地提到,工厂类从堆中分配一块内存(在我们的例子中是 20 个字节),然后用对象的数据填充该块。剩下的一个问题是这 20 个字节是 _如何 _分配的

    在 Cheney 的垃圾收集算法中,新生代(New Space)被分为两个半空间。为了在堆中分配一个内存块,分配器确定在当前半空间的 Limit,和该半空间的当前 Top 之间是否有足够的可用字节。如果有足够的空间,算法返回下一个块的地址,然后按请求的字节数递增 Top 指针

    这里展示了这种基本情况,显示了当前半空间的前后状态: 「译」JavaScript 是如何计算 1+1 的 - Part 1 创建源码字符串

    如果当前的半空间用完了可用内存(TopLimit 太接近),那么 Cheney 算法的收集部分就会开始。一旦收集完成,所有的 _活 _对象将被复制到第二个半空间的开始,而所有的 _死 _对象(残留在第一个半空间中)将被丢弃。无论怎样,一个半空间都能保证其所有 _使用过 _的空间都在底部,而所有的 _空闲的 _空间都在顶部,所以它总是会像上图一样

    不过在我们的情况下,当前的半空间有很多空闲的内存,所以我们切掉 20 个字节,然后增加 Top 指针。不需要进行垃圾收集,也不涉及第二个半空间。在 V8 代码中,有许多特殊情况需要考虑,但最后 20 个字节的分配是由 src/heap/new-spaces-inl.h 中的 NewSpace::AllocateFastUnaligned 方法处理的

    五、返回一个句柄

    现在我们有了一个指针,指向完全填充了字符串的内容(包括长度、哈希值和映射)的 Heap Block,这个指针必须返回给客户端应用程序。如果你还记得,客户端调用了这行代码

    Local<String> source = String::NewFromUtf8Literal(isolate, "1 + 1");
    

    但是,source 的类型到底是什么,Local<String> 到底是什么意思?这里有两个关键的观察点:

    将内部类转换为外部类

    首先,我们先回顾一下, V8 使用 v8::internal::SeqOneByteString 类存储了我们的字符串对象,有趣的是它只是一个指向堆上数据的指针。然而,客户端应用程序期望数据的类型是 v8::String,这是 V8 API 的一部分

    你可能会感到惊讶,v8::internal::SeqOneByteStringv8::internal::String 的一个子类)与v8::String 处于一个完全不同的类层次结构。事实上,所有的内部类都是在 src/objects 目录下使用v8::internal 命名空间定义的,而外部类则是在 include/v8.h 中使用 v8 命名空间定义的

    重温我们之前讨论过的 NewFromUtf8Literal 方法(见 src/api/api.cc),在将对象指针返回给客户端应用程序之前的最后一步是将结果从 v8::internal::String 转化为 v8::String

    return Utils::ToLocal(handle_result);
    

    这个转换是通过定义在 src/api/api-inl.h 中的宏来完成的

    管理好垃圾回收的「根」

    其次,我们来讨论一下 Local<String> 的含义(顺便说一下,它是 v8::Local<v8::String> 的缩写)。Local 的概念是当字符串对象不再被需要时,我们如何处理它的垃圾回收

    任何 JavaScript 开发人员都知道,当对象没有剩余的引用时,就会进行垃圾回收。回收算法从「根」开始,然后遍历整个堆,找到所有可到达的对象。根是一个非堆(non-heap)引用,比如一个全局变量,或者仍然在作用域中的基于堆栈(stack-based)的局部变量。如果这些变量被分配了新的值,或者它们离开了作用域(它们的封装函数结束),它们曾经指向的数据现在有可能是垃圾

    hello-world.cc 程序的情况下,我们在 C++ 栈中也有指针,可以引用堆对象。这些指针没有对应的JavaScript 变量名,因为它们只存在于 C++ 程序的上下文中(比如 hello-world.cc,或者 Chrome,或者NodeJS)。例如:

    Local<String> source = ...
    

    在这种情况下,source 是对堆对象的引用,尽管现在多了一层间接性。这张图将解释: 「译」JavaScript 是如何计算 1+1 的 - Part 1 创建源码字符串

    左边是 C++ 堆栈,随着程序的执行,堆栈从上往下增长,右边是我们前面看到的内存块。当客户端程序执行时,它会将一个 HandleScope 对象推送到本地 C++ 栈上(见 src/samples/hello-world.cc)。接下来,调用 String::NewFromUtf8Literal() 的返回值作为一个 Local<String> 对象存储在 C++ 栈上

    看起来我们又增加了一层间接性,但这样做是有好处的

    • 寻根更容易 - HandleScope 对象是一个存储堆对象的「句柄」(也就是指针)的地方。你还记得,这正是我们的 SeqOneByteString 对象,一个指向底层堆数据的 8 字节指针。当垃圾收集启动时,V8 会迅速扫描 HandleScope 对象,找到所有的根指针。然后,如果底层堆数据被移动,它可以更新这些指针。

    • **本地指针易于管理 - **与相当大的 HandleScope 相比,Local<String> 对象是 C++ 堆栈上的一个 8 字节的值,它可以和其他任何 8 字节的值(如指针或整数)在相同的上下文中使用。特别是,它可以存储在CPU 寄存器中,传递给函数,或者作为返回值提供。值得注意的是,当垃圾回收发生时,垃圾回收器不需要定位或更新这些值

    • **消除作用域很容易 - **最后,当客户端应用程序中的 C++ 函数完成后,C++ 堆栈上的 HandleScopeLocal 对象会被删除,但只有在它们 C++ 对象析构函数被调用后才会被删除。这些析构函数从垃圾收集器的根列表中删除了所有的句柄。它们不再在作用域中,所以底层堆对象可能已经成为垃圾

    最后,引用我们的 1 + 1 字符串的 source 变量,现在已经准备好在我们的客户端应用程序中传递到下一行

    Local<Script> script = 
        Script::Compile(context, source).ToLocalChecked();
    

    下一节……

    在堆上分配 1 + 1 的字符串显然有很多工作要做。希望它能说明 V8 内部架构的一些部分,以及在系统的不同部分如何表示数据。在未来的博文中,我会更多地研究我们的简单表达式是如何被解析和执行的,这将暴露出更多关于 V8 的运作方式

    在本系列博文的第 2 部分,我将深入研究 _编译缓存 _是如何工作的,以避免编译代码超过必要的时间

    附录:

    第一次翻译文章,感谢 deepL、百度翻译、谷歌翻译

    作者授权: 「译」JavaScript 是如何计算 1+1 的 - Part 1 创建源码字符串


    下载网 » 「译」JavaScript 是如何计算 1+1 的 - Part 1 创建源码字符串

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元