低代码到底是不是行业毒瘤?一线大厂怎么做的?戳此了解>>> 了解详情
写点什么

JavaScript 引擎深入剖析(一):JSValue 的内部实现

2021 年 5 月 07 日

JavaScript 引擎深入剖析(一):JSValue 的内部实现

在我们 Hummer 跨端技术框架 的研发过程中,不可避免会对 JavaScript 引擎有所探索和研究。只有深入了解了 JavaScript 的工作原理,才能在跨端研发的诸多细节上避免踩坑,并且做出更好地调优工作。


对于很多前端同学来说,JavaScript 引擎就像一个难以触及的黑盒,既熟悉又陌生,因为它被内置在了浏览器内核中。即使在平时开发过程中天天和 JavaScript 引擎打交道,但大多也只是知道 JavaScript 引擎可以解释执行 JavaScript 代码,对于其内部实现原理并不是特别了解。


所以我们接下来会专门花几个专题,来深入剖析一下 JavaScript 引擎的世界,逐步揭开它的神秘面纱。


这一期我们主要讲一下 JavaScript 引擎中的 “JSValue 的内部实现”。

前言

许多现代编程语言都具有称之为动态类型的功能。动态类型语言和静态类型语言之间的主要区别在于,大多数类型检查是在运行时执行的,而不是在编译时执行的。类型不再与变量关联,而是与内部存储的基础值关联,本文将以 JavaScript 为例进行分析。

实现方式

实现 JavaScript 引擎的第一步是实现值的表示形式,这其实有一定的难度,因为 JS值 可以是几种不同的类型中的任何一种:


  • undefined

  • null

  • boolean

  • number (double)

  • reference (string, Symbol, Object, etc)


要实现 动态类型 就需要一种能够表示上面所有类型的数据结构。实现这样的值类型主要有以下几种方式:


  • tagged 方式

  • tagged unions(QuickJS

  • tagged pointer(V8

  • boxing 方式

  • nan-boxing(JavaScriptCore

  • nun-boxing & pun-boxing(SpiderMonkey


下面分别来详细介绍下这些实现方式,以及这些方式对应的落地 JavaScript 引擎。

1. tagged unions

先来看下 QuickJS 中比较直接的一种实现方式:

QuickJS

#else /* !JS_NAN_BOXING */
typedef union JSValueUnion { int32_t int32; double float64; void *ptr;} JSValueUnion;
typedef struct JSValue { JSValueUnion u; int64_t tag;} JSValue;
#define JSValueConst JSValue
复制代码


这其实是 tag + struct 的改进版。使用 union 可减少一定的内存使用。


但缺点是不论 JSValue 表示 int32 还是 指针 类型。都需要 16 个字节(以在双精度浮点数或 64 位指针或 int64 上保持 8 字节对齐)。


那么是否有更好的 JSValue 表示方法呢?能否压缩到只用 8 字节呢?接下来我们先来看 JavaScriptCore 的实现。

2. nan-boxing

在开始之前,我们需要一些准备知识。IEEE 754 标准。在下文所提标准中,如无特殊说明,均为 IEEE 754,且以 64 架构为例。

double

关于 double 的定义可以根据 维基百科的相关链接 查看。这里我们主要摘录其格式:



  • sign: 表示正负,0 为正,1 为负

  • exponent: 指数位

  • fraction: 尾数


0.3 为例:二进制格式: 0b0011111111010011001100110011001100110011001100110011001100110011

NaN

同样,根据标准,NaN(Not a Number)的定义和种类 (NaN 同样分为两种类型:qNaNsNaN,具体请看这里) 如图:



这里简单说明下:


  • 如果 exponent 全部设置为 1,则表示为 NaN。

  • 剩余的 fraction(Mantissa) 的最左边 1 位,代表 NaN 的类型。


因此,一个 NaN 值,是有 51(64 - 11 + 1 + 1) 位未使用的。而 指针 真正也只是使用(限制)了 64 位中的 48 位。


当我们对超过 0x0000 7fff ffff ffff 的地址进行寻址时,会收到一个 EXC_I386_GPFLT 错误。


因此我们可以在剩余的 51 位中,按照一定的 规则 写入(encode)一些自定义的数据(payload),再按照同样的规则读取(decode)。


下面我们先来看下 JavaScriptCore 的实现。

JavaScriptCore

JavaScriptCore 使用了 qNaN 标准来表示,因此有 51bit 来对剩余的 payload 进行编码/解码。


   Pointer {  0000:PPPP:PPPP:PPPP             / 0002:****:****:****   Double  {         ...             \ FFFC:****:****:****   Integer {  FFFE:0000:IIII:IIII
复制代码


上面的代码表示了 JavaScriptCore 中不同值类型的范围。但是我们可以发现,这和 IEEE-754 定义的标准存在偏差。


回过头来再来看 IEEE-754 中定义的 qNaN



根据上图,我们可以得知 NaN 的范围(16 进制表示)如下:


0xfff8 xxxx xxxx xxxx  ~  0xffff xxxx xxxx xxxx


也就是说 double 的范围实际为:


0x0000 xxxx xxxx xxxx  ~  0xfff7 xxxx xxxx xxxx


JavaScriptCore 中的 double 范围 (0x0002x ~ 0xFFFCx) 明显存在偏差。


这么做的原因是 JavaScriptCore 更偏向对指针的操作。如果完全采用 IEEE-754qNaN 定义,则指针可能是下面这形式:



这样我们在使用时,就需要进行 mask 操作,来读取真正的 指针


JavaScriptCore 的这种做法,使得指针的操作变得简单高效。


那么 double 的问题如何处理呢?


The scheme we have implemented encodes double precision values by performing a 64-bit integer addition of the value 2^49 to the number. After this manipulation no encoded double-precision value will begin with the pattern 0x0000 or 0xFFFE. Values must be decoded by reversing this operation before subsequent floating point operations may be peformed.


由于 double 的范围从 0x0002x 起,因此需要进行修正 (减去 2^49)。



源码位置如下:


ALWAYS_INLINE JSValue::JSValue(EncodeAsDoubleTag, double d){    ASSERT(!isImpureNaN(d));    u.asInt64 = reinterpretDoubleToInt64(d) + JSValue::DoubleEncodeOffset;}
inline double JSValue::asDouble() const{ ASSERT(isDouble()); return reinterpretInt64ToDouble(u.asInt64 - JSValue::DoubleEncodeOffset);}
复制代码


JavaScriptCore 中所有的类型位模式设计如下:


类型encode pattern
ValEmpty0x0000 0000 0000 0000
Null0x0000 0000 0000 0002
Wasm0x0000 0000 0000 0003
ValueDeleted0x0000 0000 0000 0004
false0x0000 0000 0000 0006
true0x0000 0000 0000 0007
Undefined0x0000 0000 0000 000a
pointer0x0000 PPPP PPPP PPPP
double0x0002 xxxx xxxx xxxx
double0xFFFC xxxx xxxx xxxx
Integer0xFFFE 0000 IIII IIII


我们可以发现这里的 not a number 更想表达的是 not a double

3. nun-boxing & pun-boxing

既然 JavaScriptCore 可以选择保留对指针的直接操作,而对 double 特殊处理,那么相反,我们也可以保留 double 的原来标准,对指针进行编码。Mozilla’s SpiderMonkey 采用了这种方式,可以参考 SpiderMonkey 中对 JSValue 的定义。

SpiderMonkey

在 32 位设备平台中,SpiderMonkey 使用 nun-boxing 。其中 u 代表 unboxed 。因为非 double 类型的值,直接使用 32(tag) + 32(payload) 的方式,即:payload 的部分是 unboxed


在 x64 和类似的 64 位平台上,指针的长度超过 32 位,因此不能使用 nun-boxing 格式。取而代之的是使用 pun-boxing,17(tag) + 47(payload)。

4. tagged pointer

作为一名 iOS 开发,提起 Tagged Pointer,应该是比较熟悉的。下面先以 iOS 中的 Tagged Pointer 为例简单介绍下。


在 64 位架构中,一个指针为 8 字节(64 位),但是通常不会真正使用到所有这些位,且由于内存对齐要求的存在,低位始终为 0。高位也始终为 0 (内存访问限制)。实际上我们只是用中间这一部分的位。下面图片均来源于 WWDC



因此我们可以使用其余的部分进行标记存储,根据标记读取 payload 中数据的具体类型:



下面是 Objective-C 中的标记类型:


OBJC_TAG_NSAtom            = 0, OBJC_TAG_1                 = 1, OBJC_TAG_NSString          = 2, OBJC_TAG_NSNumber          = 3, OBJC_TAG_NSIndexPath       = 4, OBJC_TAG_NSManagedObjectID = 5, OBJC_TAG_NSDate            = 6, OBJC_TAG_7                 = 7
复制代码


再来看一下 V8。

V8

在 V8 中 JavaScript 的对象、数组、数字或者字符串都是用对象表示的,分配在 V8 堆区。这使得可以用一个指向对象的指针表示任何值。


而为了避免整数的堆内存占用,V8 使用了 Tagged Pointer 来表示其他数据。


在 32 位架构中,表示如下:


                        |----- 32 bits -----|Pointer:                |_____address_____w1|Smi:                    |___int31_value____0|
复制代码


标记位(tag bits)有双重作用:用于指示位于 V8 堆中对象的强/弱指针或一个小整数的信号。因此,整数能够直接存储在标记值中,而不必为其分配额外的存储空间。


在 64 位架构中,表示如下:


            |----- 32 bits -----|----- 32 bits -----|Pointer:    |________________address______________w1|Smi:        |____int32_value____|0000000000000000000|
复制代码


指针压缩

从 32 位切换到 64 位。这个变化带给了 Chrome 更好的安全性、稳定性和性能,但同时也带来了更多内存消耗,因为之前每个指针占用 4 个字节而现在占用是 8 个字节。


V8 的堆区包含如下:浮点值(floating point values)、字符串字符(string characters)、解析器字节码(interpreter bytecode)和标记值(tagged values)。而在检查堆区时发现,标记值占了 V8 堆区的 70%!


为了减少内存占用,V8 使用基于基地址的 32 位偏移量,代替直接存储 64 位指针。具体见 Pointer Compression in V8


压缩前的内存布局如下:(图片来源 What's happening in V8? - Benedikt Meurer



压缩后的内存布局如下:



该项技术使用也较为广泛,如最近的 2020 WWDC 上 Advancements in the Objective-C runtime,也使用了该技术。

总结

我们可以发现类 nan-boxing 的方案具有明显的优势,即不会在堆上分配 double,大大减少了缓存压力和 GC 压力等。这就是 Moz 和 JSC 选择它的原因。同时如果在 32 位架构上,Moz 和 JSC 也会分配 64 位内存来实现装箱。


而 V8 虽然会在堆上分配 double,但也针对一些常见的场景进行了优化,如 Smi(small integer),且无论在 32 位还是 64 位架构上,V8 都只需要 32 位来表示指针。


参考链接


value representation in javascript implementations

Dynamic Typing and NaN Boxing

the secret life of NaNIEEE Standard 754 Floating Point Numbers

SpiderMonkey

What's happening in V8? - Benedikt Meurer

Pointer Compression in V8

Advancements in the Objective-C runtime


作者简介


史广远:Hummer 核心成员,主要负责 Hummer 框架的 iOS 端研发工作,对 JavaScript 引擎有着非常深入的理解。


关于我们


Hummer 官网:https://hummer.didi.cn

Hummer GitHub:https://github.com/didi/Hummer

Hummer 邮箱:hummer@didiglobal.com*


延伸阅读


《滴滴开源轻量级跨端开发框架:Hummer》

《揭秘 Hummer —— 为何选择 Hummer ?》

2021 年 5 月 07 日 15:521200

评论

发布
暂无评论
发现更多内容

BI项目失败?看看是不是缺少了这几项闭环!

博文视点Broadview

OpenYurt v0.3.0 重磅发布:全面提升边缘场景下应用部署效率

阿里巴巴云原生

阿里巴巴 容器 云原生 k8s 开源项目

Java Optimizing 读书笔记(一)

绝影-大数据

【HTML】已经废弃的align(图像对齐方式)

学习委员

html html5 Web html/css 28天写作

Serverless 架构到底要不要服务器?

Serverless Devs

Java 云计算 Serverless 运维 云原生

区块链轻节点:“身”轻,责任重

华为云开发者社区

区块链 数据 数据隐私 轻节点

开发老人笔记:Git 常用命令清单

华为云开发者社区

git 代码 bug

Intel首次公布11代酷睿桌面处理器性能:8核i9斩落锐龙12核

科技新消息

如何利用策略模式避免冗长的if-else/switch分支判断代码?

码农架构

Java 学习 设计模式

数据库表数据量大读写缓慢如何优化(2)「查询分离」

我爱娃哈哈😍

数据库 大数据 架构 后端 优化

解决Windows2012 R2下安装PostgreSQL报错的问题

PostgreSQLChina

数据库 postgresql 开源

百度研究院的追星逐浪,中国科技的奋发自强

脑极体

在函数计算中到底该不该使用 VPC?

donghui

Serverless

Redis 学习笔记 03:字典

架构精进之路

redis 七日更 28天写作

即构微信小程序直播组件是什么?有哪些功能?哪些小程序类目可以使用?

ZEGO即构

高并发架构---TCP

赖猫

TCP 后端 高并发 TCP/IP 服务器开发

百度智能小程序打造购票观影一站式体验,影视宣发新玩法助力行业复苏

DT极客

避免短信接口被黑客刷取的方法

香芋味的猫丶

短信防刷 接口安全 短信验证码 短信防轰炸 短信防火墙

干货来袭!拼多多首推全新微服务进阶指南(全彩版)简直不要太香

程序员小毕

Java 架构 微服务 springboot SpringCloud

比特币矿机工作原理

v16629866266

比特币 比特币区块链

组织部干部信息管理系统开发方案,智慧党建平台建设

WX13823153201

智慧党建平台建设

为什么说“5G是第四次工业革命”,到底有哪些推动和影响?

一只数据鲸鱼

5G 物联网 数据可视化 工业物联网

自动驾驶汽车的发展史

anyRTC开发者

人工智能 自动驾驶 AI

开发更便捷 阿里云推出一站式应用研发平台EMAS 2.0

应用研发平台EMAS

阿里云 Serverless AI 低代码 移动研发平台

53w字!阿里首推系统性能优化指南太香了,堪称性能优化最优解

程序员小毕

Java 架构 性能优化 JVM 代码优化

量化交易系统开发

威掂l8929545452

区块链 系统开发 量化交易系统 交易所

Linux网络之 从 C10K 到 DPDK

赖猫

c++ Linux linux编程 C10K DPDK

TypeScript 渐进迁移指南

LeanCloud

JavaScript typescript nodejs

红牛交易所app系统开发

威掂l8929545452

区块链 系统开发 APP开发 红牛交易所

流行的后台管理系统模板总结

老魚

程序员 建站 web全栈

量化策略交易软件开发|量化策略交易系统APP开发

开發I852946OIIO

系统开发

2021 ThoughtWorks 技术雷达峰会

2021 ThoughtWorks 技术雷达峰会

JavaScript 引擎深入剖析(一):JSValue 的内部实现-InfoQ