仓颉语言运行时轻量化实践
杨勇勇
华为语言虚拟机实验室架构师,目前负责仓颉语言静态后端的开发工作
仓颉语言运行时轻量化实践
仓颉Native后端(CJNative)是仓颉语言的高性能、轻量化实现。这里的“轻量化”意指仓颉程序运行过程中占用系统资源(内存、CPU等)较少。轻量化对于运行在资源受限场景(典型如嵌入式、移动设备等)的程序具有重要意义,占用资源越少,系统负载越小,程序运行更加平稳,用户体验更好。
本文简要谈谈仓颉语言运行时如何实现轻量化。如下图1描绘了仓颉Native后端架构,包含了仓颉程序编译和运行的流程,仓颉的轻量化运行时(libcangjie-runtime.so)是其中核心组件之一。
图 1 仓颉Native后端架构
仓颉源码被仓颉编译器(cjc)编译后生成仓颉可执行文件,仓颉库被编译后生成仓颉库文件,仓颉可执行文件及其依赖的库文件和仓颉轻量运行时三部分共同部署在执行环境中运行。仓颉轻量运行时的关键作用之一是帮助仓颉应用“轻量”地运行。
仓颉语言运行时的轻量化可以归纳为如下5个方面:
- 轻量对象布局
- 轻量类库和运行时库
- 轻量用户态线程
- 轻量CFFI
- 轻量回栈
下面分别一一介绍。
1 轻量对象布局
仓颉运行时采用精简的对象布局,图2示例了64位平台上的对象布局,起始处存放对象的类型信息,最多占用一个指针宽度的内存,且在64位平台上,可以压缩到4字节。
仓颉采用这个对象布局,可以最大程度减少内存用量。
这个精简布局的得以实施,收益于几个因素:
- 只有对象的类型信息才是对象头中必不可少的数据。除此以外,如果还有空余bit,可以用于辅助运行时优化。
- 仓颉语言规范里指定了专有数据类型用作并发编程解决数据竞争的手段,从而在对象头中不需要预留用于实现锁机制的数据区。这个设计的原因是绝大部分对象在其生命周期内都不会被用于并发编程。
- 仓颉采用了Tracing GC算法而不是基于引用计数实现的自动内存管理,不需要为引用计数预留存储位置。
- 仓颉的GC算法避免了采用转发指针,而是采用转发表记录存活对象被搬移后的新旧地址映射关系,从而对象头中不需要预留转发指针的存储位置。
2 轻量类库和运行时库
从图1可知,仓颉程序运行过程需要链接相关类库以及运行时库。仓颉程序执行的最小系统由仓颉的核心库(libcangjie-std-core.so)和仓颉轻量运行时(libcangjie-runtime.so)构成。仓颉的核心库经过精心的设计剪裁,只保留了仓颉程序启动过程中必不可少的如Array、String、Atomic、Object、Exception等类型定义构成了自包含的核心类库,其余仓颉类型被有序地组织在其它类库里。仓颉程序在编译链接的过程中,除核心类库必须链接外,其余类库都可实现按需链接加载。在静态链接模式下,仓颉程序可以实现函数级别的按需链接,最大程度减少仓颉程序依赖的库文件体积。
与此同时,下述软件工程手段也被积极用于运行时库的轻量化改造:
- 虽然仓颉运行时库采用C++语言开发,但是C++异常特性被禁止使用(-fno-exceptions),可以减少异常表等二进制数据。
- 进一步,禁用dynamic_cast等特性,可以配合-fno-rtti避免生成C++类型信息。
- 减少以及避免使用C++模板库,对于必不可少的常用数据类型(如string)采用自定义方式实现,可以避免依赖C++运行时库。C++运行时库对于代码体积和内存敏感的场景显得过大。
- 通过-Os编译选项以及链接时优化(LTO)可以进一步减少仓颉运行时库的文件体积。
通过上述软件工程实践,仓颉的核心库libcangjie-std-core.so文件大小约为600KB,仓颉的运行时库libcangjie-runtime.so文件大小约为700KB.
3 轻量用户态线程
仓颉语言采用用户态轻量级线程模型。用户态线程是指由仓颉运行时实现的线程模型,线程创建过程不涉及系统调用、线程调度不需要进入内核态。用户态线程在运行过程中的内存开销和调度开销相比系统线程普遍更小,也是仓颉轻量运行时的重要特征之一。
在资源消耗上,仓颉轻量级线程可以由用户自定义仓颉线程栈大小,最低只需要KB级别。在切换调度上,仓颉轻量级线程避免用户态与内核态的频繁切入切出,单次切换开销仅需要100ns。因此,仓颉轻量级线程适用于高并发场景,对于每秒钟上千数万的单机访问量能够较快处理。
仓颉的轻量级线程模型包含了执行线程、工作任务、监视器(monitor)、处理器(processor)、与调度器等几个基本模块。各个模块承担不同的任务,使得仓颉进程可以在充分利用多核心硬件资源的基础上进一步地扩展其并发能力。
- 执行线程:用来执行真实调度任务的载体,依赖操作系统提供的线程接口。与C/C++等本地编程语言相比,仓颉语言不需要通过增加操作系统线程数来作为提高并发量的保证,只需要提供和硬件资源相当的线程即可。
- 工作任务:可在用户态调度的仓颉线程的实例,多个仓颉线程之间的调度关系是平等的,它们可以在任意执行线程间切换。未被执行的工作任务会保存在工作队列中,等待工作线程来执行。
- 处理器:负责处理执行线程与工作任务的绑定关系,每一个处理器都维护了一个本地工作队列,这个工作队列中记录了等待执行的工作任务,当某一个处理器的本地工作队列中没有任务时,它会尝试从全局队列中获取新的任务到本地队列。
- 调度器:负责处理仓颉线程的调度策略。调度器包含了工作任务的调度算法,保证工作任务可以被公平地切换与执行。
- 监视器:具有全局视野的独立线程,用来观察各个仓颉线程的状态,避免出现某一仓颉线程占据过长CPU时间片,或者事件到来时,正在等待的线程不能及时响应等情况。
程序启动后,随着仓颉轻量化线程的创建,承载工作任务的执行线程依次进入各个处理器的本地队列中等待被调度和执行。如果所有本地队列等待执行的线程数量达到上限,则后续再新增的线程被加入到全局队列中按序等待。每个处理器查找下一个可运行线程的顺序是优先从本地队列获取执行,若本地队列为空则从全局队列获取执行。为提高线程调度效率,仓颉并发模型进一步做出调度优化策略:①空闲的处理器可以“窃取”其他处理器本地队列中等待的线程;②监视器监控所有处理器的调度情况,每个处理器调度一定数量后,会从全局队列获取单个线程调度,防止全局队列中的线程被“饿死”;③监控器监控所有线程的执行情况,对于连续执行时间超过阈值的线程,则对其进行“抢占”,使其让出处理器。
仓颉轻量化线程可以在用户态完成仓颉任务的切换,因此,在高并发场景下,相比于操作操作系统线程,仓颉轻量化线程进入内核态的次数将大大降低。仓颉轻量化线程也具备私有的栈空间,并且具备动态栈扩容的能力。用户可以根据真实的业务场景,按需配置实始栈空间大小。除非是对性能极为敏感的业务场景,否则用户不需要将栈空间内存配置得很大,即便用户配置的初始栈空间大小不满足某一线程所需要的栈内存,栈扩容能力仍然可以保证业务可以正常地执行。
4 轻量CFFI (Foreign Function Interface for C)
C语言是广泛使用的系统编程语言,所以与C语言交互是所有受管语言必备的特性。在仓颉程序中,一个被foreign关键字修饰的仓颉函数对应着一个C函数的定义,这样的仓颉函数称为仓颉外部函数(Foreign Function),可以被仓颉语句直接调用,如下述示例:
foreign func foo():Unit |
对应着C语言定义的函数如下
void foo(); |
仓颉程序调用方式如下:
// 调用foo的仓颉源码 foo() |
这是非常简洁易用的CFFI语法形式。
仓颉编译器在编译阶段为调用外部函数(如上述的foo函数)插入了调用桩函数的代码,通过该桩函数为调用外部函数做准备。仓颉编译器会根据外部函数的签名生成代码,把外部函数的入口地址和入参准备好传给桩函数,在桩函数中只需完成如下基本操作:
- 保护仓颉函数调用的上下文,用于回栈过程中构建仓颉函数调用链;
- 进入GC安全区,确保仓颉线程调用外部函数的过程中可以正常与GC线程同步;
- 设置外部函数的入参;
- 执行外部函数代码;
- 离开GC安全区;
- 重置仓颉函数调用的上下文;
在仓颉中调用外部函数还有更轻量的实现形态。如果与外部函数对应的C函数执行时间较短(耗时在微秒级或更少),从而不会显著影响仓颉线程与GC的同步,那么可以使用@FastNative修饰该外部函数,实现更快的CFFI调用性能。通过@FastNative修饰的外部函数在调用过程中不需要经过桩函数处理,仓颉编译器生成的代码直接调用对应的C函数,达到接近C语言本地调用函数的性能。
5 轻量回栈
回栈(Unwinding Stack)操作是指对一个正确的调用栈通过从被调函数反向查找其调用者、以此构建部分或者全部调用链的过程。回栈操作可以从满足条件的任意某个函数发起,通过读取该函数信息以及运行栈的其它相关信息,依次找到它的调用者、以及调用者的调用者,从而构建出来所需的调用链。回栈的使用场景包括打印调用栈信息、实现异常处理、帮助GC处理栈上的根引用等多种用途。
CJNative的回栈是基于帧指针(Frame Pointer, FP)实现的,即在运行中保持栈基址寄存器始终存储当前执行帧的栈基址,并在每次调用的起点将上一帧的栈基址写入本帧的起始位置,形成每个帧都记录着上一帧的链式结构。在回栈时每次根据本栈帧的基址找到上一帧的基址,层层向上,实现回栈流程。这种实现的优势在于回栈流程简洁,速度极快,对于实现低时延的并发GC尤为必要,劣势在于必须持续占用一个物理寄存器和以及维护帧指针的额外指令开销,要求编译器生成相应的指令。另一种业界常用的回栈方式是基于CFI信息的回栈。CFI信息是编译时生成的一组额外的伪指令,描述了回栈过程中如何根据寄存器和帧数据恢复栈基址和link register等与调用链有关的寄存器。通过CFI恢复寄存器的操作需要模拟CFI指令的演算过程,从而导致回栈性能开销大、速度较慢。
综合来看,基于的FP回栈操作流程简洁,计算开销低,耗时较短(基础回栈操作耗时约零点几个微秒),在性能上有着非常好的表现,满足轻量化运行时的回栈要求。
6 小结
上述罗列了CJNative轻量化设计及实现的几个主要方面,对于其它有益于仓颉语言“高性能、轻量化”实现的重要技术,将会另文描述。我们在内部典型嵌入式场景下评估,定制的仓颉语言运行时二进制文件约700KB,应用启动时间小于10ms,空载应用(main函数中调用sleep)占用内存约800KB,验证了仓颉语言可以帮助开发者编写更轻量的应用程序、让应用更轻量地部署及运行。
相关文章:

仓颉语言运行时轻量化实践
杨勇勇 华为语言虚拟机实验室架构师,目前负责仓颉语言静态后端的开发工作 仓颉语言运行时轻量化实践 仓颉Native后端(CJNative)是仓颉语言的高性能、轻量化实现。这里的“轻量化”意指仓颉程序运行过程中占用系统资源(内存、CPU等…...
深入理解Python中的subprocess模块
目录 subprocess模块简介常用函数执行外部命令管道通信子进程管理错误处理实际应用示例最佳实践 subprocess模块简介 subprocess模块是Python标准库的一部分,提供了一个跨平台的方法来生成新进程、连接其输入/输出/错误管道,并获取其返回码。该模块旨…...

从零开始搭建 EMQX 集群压测框架
从零开始搭建 EMQX 集群压测框架 架构 在设计以EMQX为中心的MQTT消息队列集群压力测试框架时,我们采用微服务架构模式。EMQX作为消息队列的核心,负责处理MQTT协议的消息发布和订阅。Nginx作为EMQX的反向代理,负责负载均衡和SSL/TLS终端。MQT…...

ArkUI基本介绍
ArkUI:提供HarmonyOS应用UI开发框架,几件开发、精致体验、跨设备/跨平台。 ArkUI(方舟UI框架)为应用的UI开发提供了完整的基础设施,包括简洁的UI语法、丰富的UI功能(组件、布局、动画以及交互事件ÿ…...

vue2+OpenLayers 天地图上打点并且显示相关的信息(2)
上次是在地图上打点 这次鼠标移动在图标上面显示相关的信息 首先有两个事件 鼠标移入 和 鼠标移出事件 pointermove pointerout 鼠标放上去之前 放上去后 代码如下 <template><div class"container"><div id"vue-openlayers" class&quo…...

c++继承(二)
一、友元函数的继承 友元函数不能被继承,就像爸爸的朋友不是你的朋友,如果要有友元函数,在子类重新定义一个。 二、静态成员的继承 静态成员的继承仍然是那个成员,普通成员的继承是不同的。 父类的静态成员属于当前类…...

低代码开发的崛起:机遇与挑战
近年来,“低代码”开发平台的迅速崛起,已经成为IT行业中不可忽视的趋势。这些平台承诺让非专业人士也能快速构建应用程序,通过减少代码编写的需求,大幅提高开发效率。对于许多企业而言,低代码开发工具成为了一个加速数…...
Json-JacksonUtils工具类
为了创建一个通用的 Jackson 工具类,我们可以定义一个名为 JacksonUtils 的工具类,该类将提 供多种方法来支持不同类型的 JSON 转换需求。下面是一个示例实现,包括基本的 JSON 到 Java 对象的转换、Java 对象到 JSON 的转换、以及更复杂的类型如 CommonResult 的转换。 C…...
svn客户端装完后没有svn.exe
如果SVN客户端(如TortoiseSVN)安装完成后,在预期的安装目录(通常是bin目录)中没有找到svn.exe文件,这通常是因为在安装过程中没有选择安装命令行客户端工具(Command Line Client Toolsÿ…...

TinyWebserver的复现与改进(4):主线程的具体实现
GitHub - yzfzzz/MyWebServer: Linux高并发服务器项目,参考了TinyWebServer,将在此基础上进行性能改进与功能增加。为方便读者学习,附带详细注释和博客! TinyWebserver的复现与改进(1):服务器环…...
DaemonSet 不能帮助我们做什么事情?
DaemonSet 不能帮助我们做什么事情? A. 保证集群内每一个(或者一些)节点都运行一组相同的Pod B. 跟踪集群节点状态,保证新加入的节点自动创建对应的Pod C. 跟踪集群节点状态,保证移除的节点删除对应的Pod D. 能够设置Pod重试次数,…...
开源模型应用落地-LangChain高阶-记忆组件-RedisChatMessageHistory正确使用(八)
一、前言 LangChain 的记忆组件发挥着至关重要的作用,其旨在协助大语言模型(LLM)有效地留存历史对话信息。通过这一功能,使得大语言模型在对话过程中能够更出色地维持上下文的连贯性和一致性,进而能够像人类的记忆运作方式那样,进行更为自然、流畅且智能化的交互。 它仿佛…...

解决Openwrt 串口默认是没有密码的方法
将串口登录加入密码方法如下: 步骤一:配置busybox的登录,可以在.config文件中添加如下 CONFIG_BUSYBOX_CONFIG_LOGINy 添加后,需要重新编译busybox。 步骤二:修改target/linux/ramips/base-files/etc/inittab文件 将…...
【vue讲解:v-model 之 lazy、number、trim、与后端交互、小电影案例】
2 v-model 之 lazy、number、trim lazy:等待input框的数据绑定时区焦点之后再变化 number:数字开头,只保留数字,后面的字母不保留;字母开头,都保留 trim:去除首位的空格<!DOCTYPE html> …...

ECCV 2024 | 南洋理工三维数字人生成新范式:结构扩散模型
该论文作者均来自于新加坡南洋理工大学 S-Lab 团队,包括博士后胡涛,博士生洪方舟,以及计算与数据学院刘子纬教授(《麻省理工科技评论》亚太地区 35 岁以下创新者)。S-Lab 近年来在顶级会议如 CVPR, ICCV, ECCV, NeurIP…...

2024.8.13-算法学习(原创+转载)
一、什么是张量并行(Tensor Parallelism) ? 张量并行(Tensor Parallelism) 是一种分布式矩阵算法。 随着模型越来越大,模型内的矩阵也越来越大。一个大矩阵的乘法可以拆分成多个小矩阵的运算,…...

beautifulsoup的简单使用
文章目录 beautifulsoup一. beautifulsoup的简单使用1、安装2、如何使用3、对象的种类 二、beautifulsoup的遍历文档树2.1 子节点.contents 和 .children descendants2.2 节点内容.string.text 2.3 多个内容.strings**.stripped_strings** 2.4 父节点.parent.parents 三、beaut…...
【Python】Jupyter Notebook的安装及简单使用
Jupyter Notebook的安装及简单使用1、安装2、language设置为中文3、Jupyter Notebook启动4、Jupyter Notebook的常用快捷方式5、将Notebook笔记转为其他文件格式保存 Jupyter Notebook的安装及简单使用 不安装AnaCoda,但需要使用Jupyter Notebook 1、安装 pip inst…...

中国自动驾驶出租车冲击网约车市场
近年来,中国的自动驾驶技术迅速发展,对传统网约车市场构成了越来越大的冲击。随着科技巨头百度旗下的萝卜快跑等公司加速推广无人驾驶出租车,这一趋势引发了广泛的讨论和担忧。 自动驾驶技术的迅猛发展 中国自动驾驶行业正处于快速发展阶段&…...

解决浏览器书签同步问题,极空间部署开源免费的跨平台书签同步工具『xBrowserSync』
解决浏览器书签同步问题,极空间部署开源免费的跨平台书签同步工具『xBrowserSync』 哈喽小伙伴们好,我是Stark-C~ 作为一个喜欢折腾的数码党,我平时上网冲浪使用的浏览器绝不会只限于一种,就比如说我在上班的地方只会用到Edge浏…...

CMake基础:构建流程详解
目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...
C++中string流知识详解和示例
一、概览与类体系 C 提供三种基于内存字符串的流,定义在 <sstream> 中: std::istringstream:输入流,从已有字符串中读取并解析。std::ostringstream:输出流,向内部缓冲区写入内容,最终取…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...
是否存在路径(FIFOBB算法)
题目描述 一个具有 n 个顶点e条边的无向图,该图顶点的编号依次为0到n-1且不存在顶点与自身相连的边。请使用FIFOBB算法编写程序,确定是否存在从顶点 source到顶点 destination的路径。 输入 第一行两个整数,分别表示n 和 e 的值(1…...
力扣-35.搜索插入位置
题目描述 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 class Solution {public int searchInsert(int[] nums, …...

学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”
2025年#高考 将在近日拉开帷幕,#AI 监考一度冲上热搜。当AI深度融入高考,#时间同步 不再是辅助功能,而是决定AI监考系统成败的“生命线”。 AI亮相2025高考,40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕,江西、…...
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join 1、依赖1.1、依赖版本1.2、pom.xml 2、代码2.1、SqlSession 构造器2.2、MybatisPlus代码生成器2.3、获取 config.yml 配置2.3.1、config.yml2.3.2、项目配置类 2.4、ftl 模板2.4.1、…...
Caliper 负载(Workload)详细解析
Caliper 负载(Workload)详细解析 负载(Workload)是 Caliper 性能测试的核心部分,它定义了测试期间要执行的具体合约调用行为和交易模式。下面我将全面深入地讲解负载的各个方面。 一、负载模块基本结构 一个典型的负载模块(如 workload.js)包含以下基本结构: use strict;/…...
日常一水C
多态 言简意赅:就是一个对象面对同一事件时做出的不同反应 而之前的继承中说过,当子类和父类的函数名相同时,会隐藏父类的同名函数转而调用子类的同名函数,如果要调用父类的同名函数,那么就需要对父类进行引用&#…...