深入理解 V8 的 Call Stack

简介: Call Stack(调用栈) 一般指计算机程序执行时子程序之间消息处理的相互调用产生的一些列函数序列,而且几乎所有的计算机程序都依赖于调用栈。

作者:UC 国际研发 叫兽

image.png

Call Stack 与 Stack 的概念

Call Stack(调用栈) 一般指计算机程序执行时子程序之间消息处理的相互调用产生的一些列函数序列,而且几乎所有的计算机程序都依赖于调用栈。

在探讨 Call Stack 前,先来搞清楚 Stack(栈)的概念。

Stack 就是一种特殊的串列形式的数据结构,特殊之处在于只能允许在链接串列或阵列的一端(称为堆叠顶端指标,英语:top)进行加入数据(英语:push)和输出数据(英语:pop)的运算。因此栈的数据结构只允许在一端进行操作,按照后进先出(LIFO, Last In First Out)的原理运作。

image.png

Call Stack 是如何运作的

让我们看看下面的代码:

image.png

它的执行结果是:

c
b
a

该代码执行过程经历了两个阶段 首先是执行入栈。

执行 a() 方法后,此时 a 就被添加到调用栈的顶部。

image.png

在 a 内部调用了 b(),此时的调用栈顶部添加了 b:
image.png

同样 b 内部调用了 c(),此时的调用栈顶部添加了 c,最终的调用栈变成了:

image.png

此时 console.log('c'); 首先被执行。

当执行完 c 后,调用并不就此完成,开始第二阶段的出栈:

image.png

b 方法重新获得了线程控制, 执行了 console.log('b'); 。

b 执行完成,栈退到 a 方法上:

image.png

执行 console.log('a'); 。

最后调用完成,调用栈 emptied。

image.png

调用栈的大小

由于操作系统对每组线程的栈内存有一定的限制,为适应线程各种操作系统,所以 Node.js 默认的栈大小为 984k。

Slightly less than 1MB, since Windows' default stack size for the main execution thread is 1MB for both 32 and 64-bit. @src/globals.h:108:1

如何获取当前环境的调用栈大小?

image.png

不过,由于不同版本的 Node 集成的 V8 版本和优化等不同,即使同样 size 的栈空间,调用栈的栈深浅各不相同,我们尝试使用递归函数来测试一下每个版本的 Node.js 环境的可用栈深情况。
image.png

node v4.8.3

computeMaxCallStackSize 15705

node v5.12.0

computeMaxCallStackSize 15700

node v6.10.2

computeMaxCallStackSize 15718

node v7.9.0

computeMaxCallStackSize 15674

从执行结果看,虽然各个版本的调用栈空间默认都是 984kB,从 4.8.3、5.12.0 和 6.10.2 数个版本栈深度大约在 15700 以上,而 7.9.0 版的深度则为 15674。

从实际使用上看,这样的栈深表示一个线程上执行函数的调用栈可达到 15700 层,除非代码中出现"死循环"等情况,对于日常的运算基本是不会有任何问题。

但需要注意的是,调用栈的深度要根据当前调用函数的函数体大小和 local 变量的多少来决定,假如调用栈需要保存的本地变量数量较多,则需要占用较多栈空间来放置这些变量指针,那么栈深度就将远小于该值。

如果需要修改栈的大小,可以通过以下指令增加其大小:

image.png

V8 的调用栈优化

V8 为提高 JavaScript code 的运行性能,从一开始就采取激进的基于机器码编码方案,那么 V8 在处理调用栈的问题上,是否又有进行了优化呢?

我们对以上的代码进行修改,尝试对同一段代码进行 10 次重复执行。

image.png

各个版本下,我们看到输出的数据:

node v4.8.3 (v8@4.5.103.47)

image.png

node v5.12.0 (v8@4.6.85.32)

image.png

node v6.10.2 (v8@5.1.281.98)

image.png

node v7.9.2 (v8@5.5.372.43)

image.png

实验的结果,在栈大小不变的情况下,代码被重复执行 2、3 遍后,栈的深度会增加(但 6.10.2 除外,比较诡异),可以理解为栈的内存得到了优化。在而 7.9.2 的版本,运行了两次后,栈的深度更大幅增加 14.28%。

根据 V8 的优化机制,当程序进入 V8 VM 环境后,代码会首先进行简单编译(Full Compliler),这个过程为 gencode,生成机器码并后才开始执行,而 Crankshaft 的优化编译机制并不会被启动,因为此阶段对于编译器来说,看到的只是代码,还无法分析出这些代码哪部分需要优化的。

每个经过 FC 编译的函数都会包含一个计数器,当函数返回或完成一轮循环的时候,就会减少计数的值, 分析器在计数减到 0 的时候,内置性能分析器就可以挑选这类的热点函数,并启动 Crankshaft(优化编译)对其进一步的优化处理,指向其代码的指针就会被改写指向为一个 V8 内置的函数——Lazy Recompile,这样函数再次被调用时将执行经过优化的函数代码。(笔者认为:同时堆栈上的空间上用于存储的函数将被替换,指针指向了栈外的某个堆内存上,节省了栈空间的占用)。

总结

Call Stack(调用栈)实际上就是用于存储函数的一种内存数据,而且遵循 LIFO 原理实现的进栈和出栈等一系列操作。栈的大小受到操作系统的限制,一般会少于 1MB 的空间,能使用的回调栈层数受制于栈中每个栈函数的内部变量数量等不同,调用栈的深浅也不一样。

从我们的开发层面看,代码的执行和栈深一般都是有限的,所以默认的情况下代码都不会出现调用栈溢出异常的问题发生。

在了解调用栈的工作原理,及调用栈在各个版本上的运行表现后,其实我们应该思考一下,假设我需要设计一个类似 process.nextTick() 或者 co.next() 这样的函数时,应该如何设计函数方法体,让该函数的代码既有效率地执行同时又能被系统做优化处理,而什么样的代码不行的问题。

目录
相关文章
|
C++ 开发者 编译器
C/C++经典面试50题(挑重点整理)下
重点整理了C/C++经典面试题
23525 0
EMQ
|
运维 Linux 网络性能优化
MQTT 5.0 报文解析 05:DISCONNECT
在 MQTT 中,客户端和服务端可以在断开网络连接前向对端发送一个 DISCONNECT 报文,来指示连接关闭的原因。客户端发送的 DISCONNECT 报文还可以影响服务端在连接断开后的行为,例如是否发送遗嘱消息,是否更新会话过期间隔。
EMQ
866 0
MQTT 5.0 报文解析 05:DISCONNECT
|
JSON JavaScript Java
在Java中处理JSON数据:Jackson与Gson库比较
本文介绍了JSON数据交换格式及其在Java中的应用,重点探讨了两个强大的JSON处理库——Jackson和Gson。文章详细讲解了Jackson库的核心功能,包括数据绑定、流式API和树模型,并通过示例演示了如何使用Jackson进行JSON解析和生成。最后,作者分享了一些实用的代码片段和使用技巧,帮助读者更好地理解和应用这些工具。
900 0
在Java中处理JSON数据:Jackson与Gson库比较
|
调度 UED 开发者
【鸿蒙软件开发】UIAbility组件概况、生命周期与启动模式
【鸿蒙软件开发】UIAbility组件概况、生命周期与启动模式
1782 0
【鸿蒙软件开发】UIAbility组件概况、生命周期与启动模式
|
前端开发 调度
带你深入React 18源码之:useMemo、useCallback和memo
在这篇文章中,我们将探讨useMemo、useCallback和memo的用法和区别,并通过源码分析来理解它们的工作原理,开整!
带你深入React 18源码之:useMemo、useCallback和memo
|
机器学习/深度学习 自然语言处理 搜索推荐
推荐系统的算法分类和操作流程介绍
推荐系统的算法分类和操作流程介绍
|
移动开发 监控 前端开发
2023 年大淘宝 Web 端技术概览
2022 年,大淘宝前端团队进行了调整:重新组织生产关系,按业务线拆分整合进对应的业务技术团队,同时保留了大前端虚线组织,确保研发基建的一致性、技术的持续投入以及推进人员的成长。 整个变化涉及超过三百人的前端团队,经过了半年多的运转,整个团队在技术上也进行了对应的聚焦和收敛。 新的组织协作形态下,大淘宝 Web 领域的工程师们正在做哪些技术工作、有什么技术产品,特在 2023 年开年之际向行业前端同学进行分享。
5020 1
2023 年大淘宝 Web 端技术概览
|
Android开发
电脑控制安卓手机:scrcpy
电脑控制安卓手机:scrcpy
668 0
电脑控制安卓手机:scrcpy
|
前端开发
React 16.x折腾记 - (7) 基于React+Antd封装聊天记录(用到React的memo,lazy, Suspense这些)
在重构的路上,总能写点什么东西出来 , 这组件并不复杂,放出来的总觉得有点用处 一方面当做笔记,一方面可以给有需要的人; 有兴趣的小伙伴可以瞅瞅。
460 0
|
移动开发 开发框架 JavaScript
mPaas-H5容器与离线包介绍
mPaaS移动开发框架提供了一套完整的混合应用(Hybrid)开发解决方案,包括客户端的H5容器组件和服务端的离线包发布管理平台。 H5 Web应用的特点是开发效率高,但受制于Web技术和HTTP协议的限制,在移动端的表现差强人意。Native应用的优势在于性能,但是动态更新的能力较弱。Hybrid方案的出现正是为了调和这两者矛盾并发挥两者的优势,其核心技术能力包括:H5离线技术和JSBridge技术。简单来说,H5离线技术就是将HTML/CSS/JS和资源文件等打包提前下发到App中,使得App在加载H5应用时可以直接从本地读取资源文件,避免了大量的网络资源请求,从而提高H5应用的加载效率;
1506 0
mPaas-H5容器与离线包介绍