Postgresql中JIT函数能否inline的依据function_inlinable

相关
《Postgresql源码(128)深入分析JIT中的函数内联llvm_inline》
《LLVM的ThinLTO编译优化技术在Postgresql中的应用》

前置阅读:《Postgresql源码(128)深入分析JIT中的函数内联llvm_inline》

在JIT inline函数的过程中,会通过函数的bc代码,经过一系列规则、成本的判断来决定函数能否Inline,本篇重点分析这段逻辑:function_inlinable。

总结速查:

  • 入参F(llvm::Function):待inline函数
  • 入参functionStates(数组):记录了表达式计算所需要的所有函数,在function_inlinable函数内部检查的过程中,函数调用的其他函数,能inline的也会被加到这个数组中。
  • 入参worklist(数组):记录了待处理的{函数名,搜索路径},包括本次表达式计算的函数 和 在function_inlinable函数内部检查的过程中,函数调用的其他函数。
  • 入参visitedFunctions(llvm::Function的SET):处理过的函数名。
  • 入参running_instcount:经过function_inlinable的dfs搜索,包括当前函数和所有被调用者的指令数的总和。
  • 入参importVars(String SET ):全局变量 和 当前函数调用的其他函数的函数名,类似于符号表。

function_inlinable会做dfs搜索所有调用到的函数,关心函数的指令数、里面用到的全局变量的个数。

1 function_inlinable part 1

function_inlinable(...)
{
	...
  • 弱定义函数,__attribute__((weak)),不会Inline。
	if (F.isInterposable())
		return false;
  • 通常指的是C代码中有inline关键字的函数,不需要这里再inline了。
	if (F.hasAvailableExternallyLinkage())
		return false;
  • 把函数从IR文件加载到内存中使用。
	if (F.materialize())
		elog(FATAL, "failed to materialize metadata");

  • 确定函数没有NoInline属性(后文有个例子)。
	if (F.getAttributes().hasFnAttr(llvm::Attribute::NoInline))
	{
		ilog(DEBUG1, "ineligibile to import %s due to noinline",
			 F.getName().data());
		return false;
	}
  • function_references目的是为了了解当前函数引用了哪些变量和其他函数,评估它的大致复杂度。
  • 这里以 dexp函数为例展开讲下function_references的流程:
	function_references(F, running_instcount, referencedVars, referencedFunctions);

2 function_references

2.1 基础知识

  • BasicBlock 表示的是基本块类,Arugument 表示的是函数的形参,Constant 表示的是形如 i32 4 的常量,Instruction 表示的是形如 add i32 %a,%b 的指令。
  • Value 是一个非常基础的基类,一个继承于 Value 的子类表示它的结果可以被其他地方使用。
  • User代表了任何可以拥有操作数的LLVM对象。例如%1 = add i32 %a, %b是Instruction,同时也是一个User,抽象理解就是拥有操作数的一切对象都是User。
    请添加图片描述

2.2 dexp的ir

定义:

; Function Attrs: nounwind uwtable
define dso_local i64 @dexp(ptr nocapture noundef readonly %0) local_unnamed_addr #6 {
  %2 = getelementptr inbounds %struct.FunctionCallInfoBaseData, ptr %0, i64 0, i32 6, i64 0, i32 0
  %3 = bitcast ptr %2 to ptr
  %4 = load double, ptr %3, align 8
  %5 = fcmp uno double %4, 0.000000e+00
  br i1 %5, label %28, label %6

6:                                                ; preds = %1
  %7 = tail call double @llvm.fabs.f64(double %4) #22
  %8 = fcmp oeq double %7, 0x7FF0000000000000
  br i1 %8, label %9, label %12

9:                                                ; preds = %6
  %10 = fcmp ogt double %4, 0.000000e+00
  %11 = select i1 %10, double %4, double 0.000000e+00
  br label %28

12:                                               ; preds = %6
  %13 = tail call ptr @__errno_location() #23
  store i32 0, ptr %13, align 4
  %14 = tail call double @exp(double noundef %4) #20
  %15 = load i32, ptr %13, align 4
  %16 = icmp eq i32 %15, 34
  br i1 %16, label %17, label %21, !prof !11

17:                                               ; preds = %12
  %18 = fcmp une double %14, 0.000000e+00
  br i1 %18, label %19, label %20

19:                                               ; preds = %17
  tail call void @float_overflow_error() #24
  unreachable

20:                                               ; preds = %17
  tail call void @float_underflow_error() #24
  unreachable

21:                                               ; preds = %12
  %22 = tail call double @llvm.fabs.f64(double %14) #22
  %23 = fcmp oeq double %22, 0x7FF0000000000000
  br i1 %23, label %24, label %25, !prof !11

24:                                               ; preds = %21
  tail call void @float_overflow_error() #24
  unreachable

25:                                               ; preds = %21
  %26 = fcmp oeq double %14, 0.000000e+00
  br i1 %26, label %27, label %28, !prof !11

27:                                               ; preds = %25
  tail call void @float_underflow_error() #24
  unreachable

28:                                               ; preds = %25, %9, %1
  %29 = phi double [ %11, %9 ], [ %14, %25 ], [ %4, %1 ]
  %30 = bitcast double %29 to i64
  ret i64 %30
}

2.3 function_references函数

static void
function_references(llvm::Function &F,
					int &running_instcount,
					llvm::SmallPtrSet<llvm::GlobalVariable *, 8> &referencedVars,
					llvm::SmallPtrSet<llvm::Function *, 8> &referencedFunctions)
{
  • 申请32个位置的Set存放User指针,具体就是Instruction
	llvm::SmallPtrSet<const llvm::User *, 32> Visited;

	for (llvm::BasicBlock &BB : F)
	{
		for (llvm::Instruction &I : BB)
		{
			if (llvm::isa<llvm::DbgInfoIntrinsic>(I))
				continue;
  • 申请8个位置的vector存放llvm::User指针(Instruction的基类):
			llvm::SmallVector<llvm::User *, 8> Worklist;
			Worklist.push_back(&I);

  • 指令计数running_instcount(Instruction的基类):
			running_instcount++;

			while (!Worklist.empty()) {
				llvm::User *U = Worklist.pop_back_val();

  • 这条指令之前有没有被记录过:
				if (!Visited.insert(U).second)
					continue;
  • 遍历Instruction的操作数operands,操作数的基类也是User:
				for (auto &OI : U->operands()) {
					llvm::User *Operand = llvm::dyn_cast<llvm::User>(OI);
					if (!Operand)
						continue;
  • 当前拿到的操作数是一个baseblock的地址,一般是用于跳转,不需要记录:
					if (llvm::isa<llvm::BlockAddress>(Operand))
						continue;
  • 这里看到一个全局变量,需要记录到referencedVars中,并把全局变量的定义拿出来,放到Worklist里面去统计一把,比如一个全局变量定义为int a = 1,那么这一个Instruction会在下一轮循环中被统计。
					if (auto *GV = llvm::dyn_cast<llvm::GlobalVariable>(Operand)) {
						referencedVars.insert(GV);
						if (GV->hasInitializer())
							Worklist.push_back(GV->getInitializer());
						continue;
					}
  • 这里发现一个操作数是另一个函数,说明有其他函数引用,将Function指针记录到referencedFunctions中。
					if (auto *CF = llvm::dyn_cast<llvm::Function>(Operand)) {
						referencedFunctions.insert(CF);
						continue;
					}
					Worklist.push_back(Operand);
				}
			}
		}
	}
}

执行结束后:

  • running_instcount:35
    • IR中有35个指令
  • referencedVars:空
  • referencedFunctions:5个函数

dexp函数的IR分两部分:函数摘要和函数定义(index文件就是收集了bc文件中的函数摘要)

摘要:

^62 = gv: 
  (name: "dexp", summaries: 
    (function: (module: ^0, flags: 
      (linkage: external, 
       visibility: default, 
       notEligibleToImport: 0, 
       live: 0, 
       dsoLocal: 1, 
       canAutoHide: 0), 
   insts: 35, 
   funcFlags: 
     (readNone: 0, 
      readOnly: 0, 
      noRecurse: 0, 
      returnDoesNotAlias: 0, 
      noInline: 0, 
      alwaysInline: 0, 
      noUnwind: 1, 
      mayThrow: 0, 
      hasUnknownCall: 0, 
      mustBeUnreachable: 0), 
   calls: ((callee: ^302), (callee: ^157), (callee: ^277), (callee: ^54))))) ; 
   guid = 3352526880228194314

定义

$ cat float.ll | grep -A 58 '@dexp'
define dso_local i64 @dexp(ptr nocapture noundef readonly %0) local_unnamed_addr #6 {
  %2 = getelementptr inbounds %struct.FunctionCallInfoBaseData, ptr %0, i64 0, i32 6, i64 0, i32 0
  %3 = bitcast ptr %2 to ptr
  %4 = load double, ptr %3, align 8
  %5 = fcmp uno double %4, 0.000000e+00
  br i1 %5, label %28, label %6

6:                                                ; preds = %1
  %7 = tail call double @llvm.fabs.f64(double %4) #22
  %8 = fcmp oeq double %7, 0x7FF0000000000000
  br i1 %8, label %9, label %12

9:                                                ; preds = %6
  %10 = fcmp ogt double %4, 0.000000e+00
  %11 = select i1 %10, double %4, double 0.000000e+00
  br label %28

12:                                               ; preds = %6
  %13 = tail call ptr @__errno_location() #23
  store i32 0, ptr %13, align 4
  %14 = tail call double @exp(double noundef %4) #20
  %15 = load i32, ptr %13, align 4
  %16 = icmp eq i32 %15, 34
  br i1 %16, label %17, label %21, !prof !11

17:                                               ; preds = %12
  %18 = fcmp une double %14, 0.000000e+00
  br i1 %18, label %19, label %20

19:                                               ; preds = %17
  tail call void @float_overflow_error() #24
  unreachable

20:                                               ; preds = %17
  tail call void @float_underflow_error() #24
  unreachable

21:                                               ; preds = %12
  %22 = tail call double @llvm.fabs.f64(double %14) #22
  %23 = fcmp oeq double %22, 0x7FF0000000000000
  br i1 %23, label %24, label %25, !prof !11

24:                                               ; preds = %21
  tail call void @float_overflow_error() #24
  unreachable

25:                                               ; preds = %21
  %26 = fcmp oeq double %14, 0.000000e+00
  br i1 %26, label %27, label %28, !prof !11

27:                                               ; preds = %25
  tail call void @float_underflow_error() #24
  unreachable

28:                                               ; preds = %25, %9, %1
  %29 = phi double [ %11, %9 ], [ %14, %25 ], [ %4, %1 ]
  %30 = bitcast double %29 to i64
  ret i64 %30
}
  • 引用函数个数:去重后5个在这里插入图片描述
  • 指令个数:35
    在这里插入图片描述
  • 引用全局变量个数:0个

和function_references计算结果一致。

3 function_inlinable part 2

  • 记录全局变量到importVars,并增加成本:
	for (llvm::GlobalVariable* rv: referencedVars)
	{
		...
		importVars.insert(rv->getName());
		/* small cost attributed to each cloned global */
		running_instcount += 5;
	}
  • 标记当前函数已经处理过了:
	visitedFunctions.insert(&F);
  • 检查dexp调用的函数:这里会处理5个函数:
    • llvm.fabs.f64
    • __errno_location
    • exp
    • float_overflow_error
    • float_underflow_error
	for (llvm::Function* referencedFunction: referencedFunctions)
	{
		llvm::StringSet<> recImportVars;

		if (referencedFunction->materialize())
			elog(FATAL, "failed to materialize metadata");
  • 判断是不是llvm内建函数,例如循环给数组赋零有可能被clang在-O2时被优化为llvm.memset
  • dexp调用的五个函数中,只有llvm.fabs.f64是llvm内建函数:
		if (referencedFunction->isIntrinsic())
			continue;

  • 已经处理过了?
		if (!visitedFunctions.insert(referencedFunction).second)
			continue;

  • 当前函数在其他编译单元?
  • 例如__errno_location函数就在glibc中。
		if (referencedFunction->hasExternalLinkage())
		{
			llvm::StringRef funcName = referencedFunction->getName();

			/*
			 * Don't bother checking for inlining if remaining cost budget is
			 * very small.
			 */
  • inline_initial_cost默认给150。
  • subThreshold = inline_initial_cost * inline_cost_decay_factor = 150 * 0.5 = 75
			if (subThreshold < 5)
				continue;

			auto it = functionStates.find(funcName);
			if (it == functionStates.end())
			{
  • 注意functionStates数组里面包含本次表达式计算用到的所有函数,比如int4abs、dexp、slot_getsomeattrs_int、i4tod等等。
  • 这里会把需要inline的函数加到functionStates中,先不做其他处理。
				FunctionInlineState inlineState;

				inlineState.costLimit = subThreshold;
				inlineState.processed = false;
				inlineState.inlined = false;
				inlineState.allowReconsidering = false;

				functionStates[funcName] = inlineState;
				worklist.push_back({funcName, searchpath});

				ilog(DEBUG1,
					 "considering extern function %s at %d for inlining",
					 funcName.data(), subThreshold);
			}
			...
	
  • 弱定义函数,__attribute__((weak)),排除。
		if (referencedFunction->isInterposable())
			return false;

  • 递归调用function_inlinable,检查内层函数。
		if (!function_inlinable(*referencedFunction,
								subThreshold,
								functionStates,
								worklist,
								searchpath,
								visitedFunctions,
								running_instcount,
								recImportVars))
		{
			return false;
		}

		/* import referenced function itself */
		importVars.insert(referencedFunction->getName());

		/* import referenced function and its dependents */
		for (auto& recImportVar : recImportVars)
			importVars.insert(recImportVar.first());
	}

经过function_inlinable的递归调用,dfs所有会调用到的函数,最终:

  • 需要inline的函数已经都加入到functionStates中。
  • 需要Inline的{函数名字,搜索路径}在worklist中。
  • 函数名和全局变量名,全部加入到worklist。

返回true表示当前函数可以inline。

	return true;
}

4 其他

dexp

怎么拿到函数的guid:funcGUID = llvm::GlobalValue::getGUID(cfuncname);
(GUID是用函数名MD5 hash出来的)
funcGUID = 3352526880228194314

index文件中查看函数属性:

^12463 = gv: 
  (guid: 3352526880228194314, 
   summaries: 
     (function: 
       (module: ^604, 
        flags: 
          (linkage: external, 
           visibility: default, 
           notEligibleToImport: 0, 
           live: 0, dsoLocal: 1, 
           canAutoHide: 0), 
        insts: 79, 
        funcFlags: 
          (readNone: 0, 
           readOnly: 0, 
           noRecurse: 0, 
           returnDoesNotAlias: 0, 
           noInline: 1, 
           alwaysInline: 0, 
           noUnwind: 1, 
           mayThrow: 0, 
           hasUnknownCall: 0, 
           mustBeUnreachable: 0), 
        calls: ((callee: ^6190), (callee: ^59633), (callee: ^10786), (callee: ^32543)))))

这里函数被标记了noInline: 1,所以该函数不会被inline。

但是dexp为什么不能被inline呢?看起来函数不长,分支也不多,也没有标记__attribute__((noinline))

Datum
dexp(PG_FUNCTION_ARGS)
{
	float8		arg1 = PG_GETARG_FLOAT8(0);
	float8		result;

	if (isnan(arg1))
		result = arg1;
	else if (isinf(arg1))
	{
		/* Per POSIX, exp(-Inf) is 0 */
		result = (arg1 > 0.0) ? arg1 : 0;
	}
	else
	{
		errno = 0;
		result = exp(arg1);
		if (unlikely(errno == ERANGE))
		{
			if (result != 0.0)
				float_overflow_error();
			else
				float_underflow_error();
		}
		else if (unlikely(isinf(result)))
			float_overflow_error();
		else if (unlikely(result == 0.0))
			float_underflow_error();
	}

	PG_RETURN_FLOAT8(result);
}

原因是这里llvm是按O2编译的,按O0编译后noInline: 0

^10363 = gv: 
  (guid: 3352526880228194314, summaries: 
    (function: 
      (module: ^604, 
       flags: 
        (linkage: external, visibility: default, 
         notEligibleToImport: 0, live: 0, 
         dsoLocal: 1, canAutoHide: 0), 
      insts: 35, 
      funcFlags: 
        (readNone: 0, 
         readOnly: 0, 
         noRecurse: 0, 
         returnDoesNotAlias: 0, 
         noInline: 0, 
         alwaysInline: 0, 
         noUnwind: 1, 
         mayThrow: 0, 
         hasUnknownCall: 0, 
         mustBeUnreachable: 0), 
      calls: ((callee: ^49065), (callee: ^8990)))))

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【C#】学习获取程序执行路径,Gemini 帮助分析

一、前言&#xff1a; 在Delphi中&#xff0c;如果想要获取当前执行程序的目录&#xff0c;程序代码如下&#xff1a; ExtractFilePath(ParamStr(0)); 今天在分析一个别人做的C#程序时看到了一段C#代码&#xff0c;意思是获取执行程序所在的文件目录&#xff1a; public stat…

Spring Boot集成dubbo快速入门Demo

1.什么是dubbo&#xff1f; Apache Dubbo 是一款微服务开发框架&#xff0c;它提供了 RPC通信 与 微服务治理 两大关键能力。这意味着&#xff0c;使用 Dubbo 开发的微服务&#xff0c;将具备相互之间的远程发现与通信能力&#xff0c; 同时利用 Dubbo 提供的丰富服务治理能力…

(教程)gpt-4o如何使用,怎么体验?gpt-4o和gpt-4-turbo的区别

今天OpenAI发布了gpt-4o&#xff0c;我体验之后&#xff0c;gpt-4o简直逆天了。中文能力也挺别强。速度比现在的gpt4还要快。 早在 5 月 11 日&#xff0c;Sam 就在推文中表示&#xff1a;OpenAI 并没有推出 GPT-5&#xff0c;或搜索引擎&#xff0c;但团队一直在努力研发一些…

halo博客--解决恶意刷评论的问题

原文网址&#xff1a;halo博客--解决恶意刷评论的问题_IT利刃出鞘的博客-CSDN博客 简介 本文介绍halo博客如何通过设置评论次数来解决恶意刷评论的问题。 评论功能要设置频率的限制&#xff0c;否则可能被人一直刷评论&#xff0c;然后数据库存的垃圾评论越来越多&#xff0…

51 单片机[2-2]:LED闪烁

摘要&#xff1a; 本文使用STC89C52RC单片机实现单个LED闪烁 新建一个项目&#xff0c;具体步骤见[2-1] 分析&#xff1a; 要使 LED 闪烁&#xff08;以D1为例&#xff09;&#xff0c;就要先让 P2 0xfe; 再让 P2 0xff; 先在keil5中把程序写成这样&#xff1a; #include &…

答辩PPT框架如何搭建?文心一言AI辅助构建

很多快要毕业的同学在做答辩PPT的时候总是感觉毫无思路&#xff0c;一窍不通。但这并不是你们的错&#xff0c;对于平时没接触过相关方面&#xff0c;第一次搞答辩PPT的人来说&#xff0c;这是很正常的一件事。一个好的答辩PPT可以根据以下分为以下几部分来写。 1.研究的背景和…

#自学习# 记一次py脚本打开浏览器页面

在项目总结中&#xff0c;遇到系统后台利用浏览器拉起一个已知路径页面的需求&#xff0c;趁着机会整理下。实现起来比较简单&#xff0c;浏览器默认谷歌。 一、技术原理 Selenium&#xff1a;Selenium 是一个用于自动化 Web 浏览器的工具&#xff0c;可模拟用户在浏览器中的各…

pnpm:无法加载文件 C:\Users\PC\AppData\Roaming\npm\pnpm.ps1,因为在此系统上禁止运行脚本。

使用pnpm命令启动vue时报了个错&#xff1a; 解决起来也简单&#xff0c;右击开始菜单&#xff0c;用管理员身份打开终端。win11的如下图&#xff1a; win10我记得应该是PowerShell&#xff08;管理员&#xff09;&#xff0c;这样的。 打开之后执行命令&#xff1a; set-…

cpu缓存一致性问题---cache写策略

为什么会有cpu缓存一致性问题&#xff1f; cpu缓存一致性指的&#xff1a;是缓存中和所其对应在主存中的数据的一致性。 因为cpu运算产生新数据后基于写回策略只更新缓存的值会导致缓存和主存不一致问题 解决cpu缓存一致性问题前&#xff0c;先了解写回策略是什么 了解写回策…

代码随想录——在每个树行中找最大值(Leetcode515)

题目链接 层序遍历 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right) …

优雅谈论大模型8:神经网络与矩阵

向量与矩阵 上个章节的神经网络是为了解Transformer或者Mamba做好铺垫&#xff0c;在和后辈交流过程中发现有个障碍&#xff0c;那就是向量和矩阵。其实向量和矩阵的表达方式不是所有人都很习惯。在继续下面的章节之前小编认为有必要将向量、矩阵和神经网络做下补充解释。 向…

【并发程序设计】4. exec函数族

4.exec函数族 exec函数族是一组用于在进程中启动另一个程序来替换当前进程的函数。 exec函数族主要用于在当前进程内部执行一个新的程序&#xff0c;而不会创建新的进程。 子进程调用exec函数&#xff0c;族父进程不受影响。进程当前内容被指定的程序替换&#xff0c;但进程…

JAVA实验项目(二): 抽象类、接口的定义与使用

实验项目二 抽象类、接口的定义与使用 Tips&#xff1a;"分享是快乐的源泉&#x1f4a7;&#xff0c;在我的博客里&#xff0c;不仅有知识的海洋&#x1f30a;&#xff0c;还有满满的正能量加持&#x1f4aa;&#xff0c;快来和我一起分享这份快乐吧&#x1f60a;&…

Python游戏开发库:开启游戏编程之旅

引言 Python凭借其简洁明了的语法和丰富的库支持&#xff0c;在游戏开发领域占有一席之地。对于希望进入游戏编程世界的开发者来说&#xff0c;Python提供了一个理想的起点。本文将介绍几个关键的Python游戏开发库&#xff0c;帮助您开启游戏编程之旅。 第一部分&#xff1a;P…

Selenium自动操作鼠标的方法及示例(鼠标左右键单击、左键双击、拖动等)

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

【线性系统理论】笔记一

一&#xff1a;状态空间表达式 电路系统状态空间描述列写 1&#xff1a;选取状态变量 状态变量定义&#xff1a;线性无关极大组属性。 2&#xff1a;列出电路原始回路方程 ps&#xff1a;状态变量有两个&#xff0c;理论上需要列写2个方程 3&#xff1a;规范形势 4&#xf…

基于Springboot的知名作家信息管理系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的知名作家信息管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系…

计算思维的理解

2006年&#xff0c;卡内基梅隆大学周以真教授首次系统性地定义了计算思维。这一年&#xff0c;她在美国计算机权威期刊《Communications of the ACM》上发表了题为《Computational Thinking》的论文&#xff0c;由此开启了计算思维大众化的全新历程。 周以真&#xff08;Jeanne…

3.3 整型

本节必须掌握的知识点&#xff1a; 整型数据类型的取值范围 示例八 代码分析 汇编解析 获取数据类型的取值范围 3.3.1 整型数据类型取值范围 整型是用来表示限定范围内连续整数的数据类型。表3-1列出了C语言编译器定义的整型数据类型及其大小和取值范围。 类型 存储大小…

三菱FX3U-4AD模拟量电压输入采集实例

硬件&#xff1a;&#xff30;&#xff2c;&#xff23;模块 &#xff26;&#xff38;&#xff13;&#xff27;&#xff21;-&#xff12;&#xff14;&#xff2d;&#xff34; &#xff1b;&#xff21;&#xff0f;&#xff24;模块&#xff26;&#xff38;&#xff13…