lua vm 五: upvalue

前言

在 lua vm 中,upvalue 是一个重要的数据结构。upvalue 以一种高效的方式实现了词法作用域,使得函数能成为 lua 中的第一类值,也因其高效的设计,导致在实现上有点复杂。

函数 (proto) + upvalue 构成了闭包(closure),在 lua 中调用一个函数,实际上是调用一个闭包。upvalue 就相当于函数的上下文。

这种带 “上下文” 的函数,也导致了热更新的麻烦,可以说是麻烦透顶了。没法简单的通过替换新的函数代码来更新一个旧闭包,因为旧闭包上可能带着几个 upvalue,这几个 upvalue 的值可能已经发生改变,或者也被其他的函数引用着。


图1:函数与upvalue

所以,要更新一个旧闭包,得把旧闭包上的所有 upvalue 都找出来,绑定到新函数上,形成一个新闭包,再用这个新闭包替换旧闭包。

本文主要讲 upvalue 在 lua vm 中的实现,下篇文章再讲如何解决带有 upvalue 的闭包的热更新问题。

下文分析基于 lua5.4.6。


1. upvalue


1.1 upvalue 实现上要解决的问题

upvalue 就是外部函数的局部变量,比如下面的函数定义中,var1 就是 inner 的一个 upvalue。

local function getf(delta)local var1 = 100local function inner()return var1+deltaendreturn inner
endlocal f1 = getf(10)

upvalue 复杂的地方在于,在离开了 upvalue 的作用域之后,还要能够访问得到。比如上面调用了 local f1 = getf(10)var1 是在 getf 的栈上分配的,getf 返回后,栈空间被抹掉,但 inner 还要能访问 var1,所以要想办法把它捕捉下来。


1.2 upvalue 的实现

下面先讲 lua 闭包的 upvalue,最后再讲 c 闭包的,因为复杂性几乎都在 lua 闭包这里面了。


1.2.1 upvalue 相关的结构体

与 upvalue 相关的结构体有:

1、UpVal,可以说是 upvalue 的本体了,很巧妙的结构,运行时用到的变量。

typedef struct UpVal {CommonHeader;union {TValue *p;  /* points to stack or to its own value */ptrdiff_t offset;  /* used while the stack is being reallocated */} v;union {struct {  /* (when open) */struct UpVal *next;  /* linked list */struct UpVal **previous;} open;TValue value;  /* the value (when closed) */} u;
} UpVal;

2、Upvaldesc,这个是编译时产生的信息,Proto 结构体就包含 Upvaldesc* 类型的数组:upvalues,用于描述当前函数用到的 upvalue 信息。

typedef struct Upvaldesc {TString *name;  /* upvalue name (for debug information) */lu_byte instack;  /* whether it is in stack (register) */lu_byte idx;  /* index of upvalue (in stack or in outer function's list) */lu_byte kind;  /* kind of corresponding variable */
} Upvaldesc;typedef struct Proto {...Upvaldesc *upvalues;  /* upvalue information */...
} Proto;

3、lua_State 中的 openupval 字段,它是 UpVal* 类型的链表,它相当于一个 cache,保存当前栈上还存活着的被引用到的 upvalue。

struct lua_State {...UpVal *openupval;  /* list of open upvalues in this stack */...
};

4、LClosure 中的 upvals 数组。

typedef struct LClosure {ClosureHeader;struct Proto *p;UpVal *upvals[1];  /* list of upvalues */
} LClosure;

1.2.2 upvalue 的访问

upvalue 是间接访问的,LClosure 结构体的 upvals 字段是 UpVal* 类型的数组。访问的时候先通过 upvals 获得到 UpVal 指针,再通过 UpVal 里面的 v.p 去访问具体的变量,伪码如下:

UpVal* UpValPtr = closure->upvals[upidx];
TValue* p = UpValPtr->v.p;

需要这样间接访问,主要是因为 UpVal 本身会随着函数调用的返回发生状态的变化:从 open 改为 close,这时它的值也从栈上被拷贝到了 “自己身上”,所以指针(v.p)是变化的,不能写死。

至于为什么会发生 open 到 close 的变化,后面会讲。


1.2.3 upvalue 的创建

upvalue 是在编译的时候计算好一个 Proto 需要什么 upvalue,相关信息存放在 Proto 的 upvalues 数组( Upvaldesc *upvalues; /* upvalue information */)中的。

举个例子,对于这样一个脚本,内部的函数 f1、f2 既引用了 getf 之外的变量 var1,也引用了 getf 之内的变量 var2、var3,并且在 local f1, f2 = getf() 调用完成后,f1 还要能访问到 var1、var2,f2 还要能访问到 var1、var3。


local var1 = 1local function getf()local var2 = 2local var3 = 3local function f1()return var1 + var2endlocal function f2()return var1 + var3endreturn f1, f2
endlocal retf1, retf2 = getf()

编译结果是:


图2:upvalue 编译信息

从编译结果可以看到,每个 Proto 都会生成 UpvalueDesc 数组,用于描述这个函数(proto)会用到的 upvalue。

index 表示在 LClosure 的 upvals 数组中是第几个。
name 表示变量名。
instack 表示这个 upvalue 是否刚好是上一层函数的局部变量,比如 var2 是 f1 的上一层的,所以 instack 为 true,而 var1 是上两层的,所以为 false。
idx 表示 instack 为 false 的情况下,可以在上一层函数的 upvals 数组的第几个找到这个 upvalue。
kind 表示 upvalue 类型,一般都是 VDKREG,即普通类型。


补充说明,kind 是 lua5.4 才整出来的,lua5.3 及之前都只有 VDKREG。5.4 新增了 RDKCONST,RDKTOCLOSE,RDKCTC。

RDKCONST 是对应到 <const>,指定变量为常量。
RDKTOCLOSE 是对应到 <close>,指定变量为 to be closed 的(类似于 RAII 特性,超出作用域后执行 __close 元函数)。
RDKCTC 我也闹不清楚。


从例子上可以看到,f1 引用了上一层函数 getf 的局部变量 var2,所以它的 instack 值是 true,而引用了上两层的局部变量 var1,则它的 instack 是 false。

instack 主要就是在创建 Closure 的时候帮助初始化 Closure 的 upvals 数组,对于 instack 为 true 的 upvalue,直接搜索上一层函数的栈空间即可,对于 instack 为 false 的 upvalue,就不能这样了,为什么呢?因为上两层的有可能已经不在栈上了。能想象得到吗?举个例子:

local function l1()local var1 = 1local function l2()local var2 = 2local function l3()return var1+var2+3endreturn l3endreturn l2
endlocal ret_l2 = l1()local ret_l3 = ret_l2()

调用 l1 的时候,得到了 l2,这时候 l1 已经返回了,它的栈已经回收了,这时候再调用 l2,在创建 l3 这个闭包的时候,是不可能再找到 l1 的栈去搜索 var1 这个变量的。

所以,要解决这个问题,就需要让 l2 在创建的时候,先帮忙把 var1 捕捉下来保存到自己的 upvals 数组中,等 l3 创建的时候,就可以从 l2 的 upvals 数组中找到了。

这正是 pushclosure 干的活:

static void pushclosure (lua_State *L, Proto *p, UpVal **encup, StkId base,StkId ra) {int nup = p->sizeupvalues;Upvaldesc *uv = p->upvalues;int i;LClosure *ncl = luaF_newLclosure(L, nup);ncl->p = p;setclLvalue2s(L, ra, ncl);  /* anchor new closure in stack */for (i = 0; i < nup; i++) {  /* fill in its upvalues */if (uv[i].instack)  /* upvalue refers to local variable? */ncl->upvals[i] = luaF_findupval(L, base + uv[i].idx);else  /* get upvalue from enclosing function */ncl->upvals[i] = encup[uv[i].idx];luaC_objbarrier(L, ncl, ncl->upvals[i]);}
}

函数实现可以看到,instack 为 true 时,调用 luaF_findupval 去上一层函数的栈上搜索,instack 为 false 时,上一层函数已经帮忙捕捉好了,直接从它的 upvals 数组(即这里的 encup 变量中)索引。

这里 uv[i].idx 就是上面 upvaldesc 的 idx 列,即当 instack 为 false 时,它对应于上一层函数的 upvals 数组的第几项。


1.2.4 upvalue 的变化:从 open 到 close

分两个阶段讲,getf 调用时以及 getf 调用后。

1、getf 调用时,var2、var3 这两个变量作为 f1, f2 的 upvalue,它们还处在 getf 的栈上,这时候它们会被放在 lua_State 的 openupval 链表中。

2、getf 调用后,它的栈要被收回的,这时候 lua vm 会调用 luaF_close 来关闭 getf 栈上被引用的 upvalue,最终是 luaF_closeupval 这个函数执行:

void luaF_closeupval (lua_State *L, StkId level) {UpVal *uv;StkId upl;  /* stack index pointed by 'uv' */while ((uv = L->openupval) != NULL && (upl = uplevel(uv)) >= level) {TValue *slot = &uv->u.value;  /* new position for value */lua_assert(uplevel(uv) < L->top.p);luaF_unlinkupval(uv);  /* remove upvalue from 'openupval' list */setobj(L, slot, uv->v.p);  /* move value to upvalue slot */uv->v.p = slot;  /* now current value lives here */if (!iswhite(uv)) {  /* neither white nor dead? */nw2black(uv);  /* closed upvalues cannot be gray */luaC_barrier(L, uv, slot);}}
}

要理解这个函数,就要知道 StkId level 这个参数的意义,它在这里是 getfbase 指针,即它的栈底。同个 lua_State 的函数调用链上的所有函数共用一个栈,按顺序各占一段栈空间,栈是一个数组,所以后调用的函数的变量在栈上的索引是更大的,表现上就是指针值更大。而 openupval 链表里面 Upval 里的 p 就是指向这指针,所以遍历 openupval 的时候,遇到 p 比 base 大的,就表明这个是 getf 栈上的变量,要把它 close 掉。

close 的操作就是把 upval 从 openupval 链表移掉,同时把 upval 的 p 指向的值拷贝到它自身上。


图3:upvalue close 时的拷贝


1.2.5 C 闭包中的 upvalue

C 闭包(CClosure)也是有 upvalue 的,是在 lua_pushcclosure 时设置的,但用的是值拷贝,所以多个 C 闭包不能共享 upvalue。如果要在多个 C 闭包,只能是各自的upvalue 指向同一个 table 这样的变量。

CClosure 的 upvalue 直接用的是 TValue 类型的数组(不是指针),在创建的时候用的值拷贝。

typedef struct CClosure {ClosureHeader;lua_CFunction f;TValue upvalue[1];  /* list of upvalues */
} CClosure;

2. 参考

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/344843.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C++ STL】模拟实现 string

标题&#xff1a;【C :: STL】手撕 STL _string 水墨不写bug &#xff08;图片来源于网络&#xff09; C标准模板库&#xff08;STL&#xff09;中的string是一个可变长的字符序列&#xff0c;它提供了一系列操作字符串的方法和功能。 本篇文章&#xff0c;我们将模拟实现STL的…

数据结构---树与二叉树

个人介绍 hello hello~ &#xff0c;这里是 code袁~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f981;作者简介&#xff1a;一名喜欢分享和记录学习的…

【中国开源生态再添一员】天工AI开源自家的Skywork

刚刚看到《AI高考作文出圈&#xff0c;网友票选天工AI居首》&#xff0c;没想到在Huggingface中发现了Skywork大模型。天工大模型由昆仑万维自研&#xff0c;是国内首个对标ChatGPT的双千亿级大语言模型&#xff0c;天工大模型通过自然语言与用户进行问答式交互&#xff0c;AI生…

Linux系统管理:虚拟机Almalinux 9.4 安装

目录 一、理论 1.Almalinux 二、实验 1.虚拟机Almalinux 9.4 安装准备阶段 2.安装Almalinux 9.4 3.Termius远程连接 一、理论 1.Almalinux (1) 简介 Almalinux是一个开源、社区拥有和管理、免费的企业Linux发行版。专注于长期稳定性&#xff0c;并提供强大的生产级…

机器学习--损失函数

损失函数&#xff08;Loss Function&#xff09;&#xff0c;也称为代价函数&#xff08;Cost Function&#xff09;或误差函数&#xff08;Error Function&#xff09;&#xff0c;是机器学习和统计学中的一个重要概念。它用于量化模型预测值与真实值之间的差异。损失函数的值…

Python一些小操作

矢量图 from matplotlib_inline import backend_inline backend_inline.set_matplotlib_formats(svg)matplotlib中文问题 import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"]["SimHei"] #设置字体 plt.rcParams["axes.unicode_minus…

docker部署redis实践

1.拉取redis镜像 # 拉取镜像 sudo docker pull redis2.创建映射持久化目录 # 创建目录 sudo mkdir -p $PWD/redis/{conf,data}3. 运行redis 容器&#xff0c;查看当前redis 版本号 # 运行 sudo docker run --name redis -d -p 6379:6379 redis # 查看版本号 sudo docker ex…

力扣每日一题129:从根节点到叶子节点的和

题目 中等 相关标签 相关企业 给你一个二叉树的根节点 root &#xff0c;树中每个节点都存放有一个 0 到 9 之间的数字。 每条从根节点到叶节点的路径都代表一个数字&#xff1a; 例如&#xff0c;从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。 计算从根节…

【Java】单例设计模式

单例设计模式简介 目录 1.单例设计模式是什么&#xff1f;2.单例设计模式设计方法饿汉式懒汉式 3.单例设计模式的应用任务管理器(仅有一个页面&#xff0c;不可多开)Runtime运行环境 1.单例设计模式是什么&#xff1f; 设计模式 是解决 特定问题的优秀设计方式之一。 单例设计…

基于springboot的教学管理系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;教师管理&#xff0c;学生管理&#xff0c;课程管理 教师账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;学生管理&#xff0c;课程管理&#xff0c;课程表信…

LibreOffice电子表格如何实现快速筛选并将结果放到新的工作表

如果是在excel或者wps中&#xff0c;可能大家都习惯了自动筛选&#xff0c;然后复制到新的工作表或者删除掉复制内容的办法。但是在LibreOffice中&#xff0c;经测试&#xff0c;大数据表的删除或者复制是非常慢的。这也是很多人放弃LibreOffice的原因之一。那么我们如何快速筛…

ArcGIS for js 4.x 加载图层

二维&#xff1a; 1、创建vue项目 npm create vitelatest 2、安装ArcGIS JS API依赖包 npm install arcgis/core 3、引入ArcGIS API for JavaScript模块 <script setup> import "arcgis/core/assets/esri/themes/light/main.css"; import Map from arcgis…

关于多线程

并发编程 在计算机的操作系统中,我们了解到了进程管理,有了解到了cpu的特性,核心数和频率,在次之前我们所写的代码都是只用到了一个核心,此时无论你怎么优化代码,最多也只能使用到一个cpu的核心,把这个核心跑满了,其他的核心也是闲着,所以我们可以通过特殊的编写代码,把多个CP…

搭建python虚拟环境,并在VSCode中使用

创建环境 python -m venv E:\python\flask\venv激活环境 运行下图所示的bat文件 退出环境 执行下面的语句 deactivateVSCode中配置&#xff1a; ①使用CTRLshiftp命令&#xff0c;使用CTRLshiftp命令&#xff0c;输入&#xff1a; Python: Select Interpreter②选择之前创建…

数据库-列的完整性约束-概述

引言 我们都知道人以群分 &#xff0c;但分为 若按照 人类的皮肤分类 黄种人&#xff08;其实是西方人定义&#xff09;我们虽然不承认也不否定 &#xff0c;黑皮肤 &#xff0c;棕色人种&#xff08;在南太平洋和西太&#xff09;白种人 排名你懂的 这好像是枚举类型 emm 尴尬…

【线性代数】向量空间,子空间

向量空间 设V为n维向量的集合&#xff0c;如果V非空&#xff0c;且集合V对于向量的加法以及数乘两种运算封闭&#xff0c;那么就称集合V为向量空间 x&#xff0c;y是n维列向量。 x 向量组等价说明可以互相线性表示 向量组等价则生成的向量空间是一样的 子空间 例题18是三位向…

三、【源码】Mapper XML的解析和注册使用

源码地址&#xff1a;https://github.com/mybatis/mybatis-3/ 仓库地址&#xff1a;https://gitcode.net/qq_42665745/mybatis/-/tree/03-parse-mapperXML Mapper XML的解析和注册使用 流程&#xff1a; 1.Resources加载MyBatis配置文件生成Reader字符流 2.SqlSessionFact…

考虑风光场景生成的电动汽车并网优化调度【遗传算法】【IEEE33】

目录 主要内容 部分代码 部分结果 下载链接 主要内容 程序主要内容是考虑风光场景生成的电动汽车并网优化调度&#xff0c;采用的方法如下所述&#xff1a; ①采用蒙特卡洛方法&#xff0c;结合copula函数以及fuzzy-kmeans&#xff0c;获取6个典型风光出力场景&…

【Pytorch】计算机视觉项目——卷积神经网络TinyVGG模型图像分类(如何使用自定义数据集)

目录 一、前言二、工作流程回顾三、详细步骤流程1. 环境配置2. 数据准备数据集下载数据存储结构&路径查看图片 3. 数据转换4. 自定义数据集&#xff08;Custom Dataset &#xff09;4.1 方法一&#xff1a;使用ImageFolder加载数据集信息查看张量转图片创建DataLoader 4.2 …

计算机毕业设计项目、管理系统、可视化大屏、大数据分析、协同过滤、推荐系统、SSM、SpringBoot、Spring、Mybatis、小程序项目编号1-500

大家好&#xff0c;我是DeBug&#xff0c;很高兴你能来阅读&#xff01;作为一名热爱编程的程序员&#xff0c;我希望通过这些教学笔记与大家分享我的编程经验和知识。在这里&#xff0c;我将会结合实际项目经验&#xff0c;分享编程技巧、最佳实践以及解决问题的方法。无论你是…