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+00br i1 %5, label %28, label %66: ; preds = %1%7 = tail call double @llvm.fabs.f64(double %4) #22%8 = fcmp oeq double %7, 0x7FF0000000000000br i1 %8, label %9, label %129: ; preds = %6%10 = fcmp ogt double %4, 0.000000e+00%11 = select i1 %10, double %4, double 0.000000e+00br label %2812: ; preds = %6%13 = tail call ptr @__errno_location() #23store 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, 34br i1 %16, label %17, label %21, !prof !1117: ; preds = %12%18 = fcmp une double %14, 0.000000e+00br i1 %18, label %19, label %2019: ; preds = %17tail call void @float_overflow_error() #24unreachable20: ; preds = %17tail call void @float_underflow_error() #24unreachable21: ; preds = %12%22 = tail call double @llvm.fabs.f64(double %14) #22%23 = fcmp oeq double %22, 0x7FF0000000000000br i1 %23, label %24, label %25, !prof !1124: ; preds = %21tail call void @float_overflow_error() #24unreachable25: ; preds = %21%26 = fcmp oeq double %14, 0.000000e+00br i1 %26, label %27, label %28, !prof !1127: ; preds = %25tail call void @float_underflow_error() #24unreachable28: ; preds = %25, %9, %1%29 = phi double [ %11, %9 ], [ %14, %25 ], [ %4, %1 ]%30 = bitcast double %29 to i64ret 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+00br i1 %5, label %28, label %66: ; preds = %1%7 = tail call double @llvm.fabs.f64(double %4) #22%8 = fcmp oeq double %7, 0x7FF0000000000000br i1 %8, label %9, label %129: ; preds = %6%10 = fcmp ogt double %4, 0.000000e+00%11 = select i1 %10, double %4, double 0.000000e+00br label %2812: ; preds = %6%13 = tail call ptr @__errno_location() #23store 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, 34br i1 %16, label %17, label %21, !prof !1117: ; preds = %12%18 = fcmp une double %14, 0.000000e+00br i1 %18, label %19, label %2019: ; preds = %17tail call void @float_overflow_error() #24unreachable20: ; preds = %17tail call void @float_underflow_error() #24unreachable21: ; preds = %12%22 = tail call double @llvm.fabs.f64(double %14) #22%23 = fcmp oeq double %22, 0x7FF0000000000000br i1 %23, label %24, label %25, !prof !1124: ; preds = %21tail call void @float_overflow_error() #24unreachable25: ; preds = %21%26 = fcmp oeq double %14, 0.000000e+00br i1 %26, label %27, label %28, !prof !1127: ; preds = %25tail call void @float_underflow_error() #24unreachable28: ; preds = %25, %9, %1%29 = phi double [ %11, %9 ], [ %14, %25 ], [ %4, %1 ]%30 = bitcast double %29 to i64ret 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_locationexpfloat_overflow_errorfloat_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();elsefloat_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)))))
相关文章:
Postgresql中JIT函数能否inline的依据function_inlinable
相关 《Postgresql源码(128)深入分析JIT中的函数内联llvm_inline》 《LLVM的ThinLTO编译优化技术在Postgresql中的应用》 前置阅读:《Postgresql源码(128)深入分析JIT中的函数内联llvm_inline》 在JIT inline函数的过…...
存储过程作为初始化数据例子
查询出每个人员,为每个人员插入11条数据,作为初始化数据 oracle存储过程 CREATE OR REPLACE PROCEDURE initialize_order_warn_config ISv_id NUMBER; BEGINSELECT COALESCE(MAX(id), 0) INTO v_id FROM order_warn_config;FOR rec IN (SELECT DISTINCT…...
【数据分析】 JupyterNotebook安装及使用简介
各位大佬好 ,这里是阿川的博客 , 祝您变得更强 个人主页:在线OJ的阿川 大佬的支持和鼓励,将是我成长路上最大的动力 阿川水平有限,如有错误,欢迎大佬指正 在数据分析中,一般用Pycharm编辑代…...
nginx命令大全
以下是Nginx的一些常用命令,适用于Linux环境,部分命令在Windows系统下也适用,但命令形式可能有所不同: 查看Nginx版本号: nginx -v:简短显示版本号。nginx -V:详细显示版本号及编译配置信息。 启动Nginx…...
【数据结构】顺序表与链表的差异
顺序表和链表都是线性表,它们有着相似的部分,但是同时也有着很大的差异。 存储空间上的差异: 对于插入上的不同点,顺序表在空间不够时需要扩容,而如果在使用realloc函数去扩容,会有原地扩容和异地扩容两种情…...
小程序如何进行评分评价
小程序以其便捷、快速、无需安装的特点,成为了众多企业、品牌与消费者之间的重要连接桥梁。而评价评分机制,作为小程序中不可或缺的一环,对于提升用户体验、建立用户信任、促进商家与用户的互动等方面,都具有至关重要的意义。本文…...
【MATLAB源码-第206期】基于matlab的差分进化算法(DE)机器人栅格路径规划,输出做短路径图和适应度曲线。
操作环境: MATLAB 2022a 1、算法描述 差分进化算法(Differential Evolution, DE)是一种有效的实数编码的进化算法,主要用于解决实值函数的全局优化问题。本文将详细介绍差分进化算法的背景、原理、操作步骤、参数选择以及实际应…...
Python图形界面(GUI)Tkinter笔记(三):控件的定位(1)
Tkinter(GUI)设计图形界面时有三种控件的包装方法去定位各控件在窗口(父容器、根窗口)上的位置。 【1】pack()方法:用方位来定位位置,类似于Word文档中的文字对齐方式。 【2】grid()方法:用二维表格式的坐标值定位,类似于EXCEL单位元。 【3】place()方法:用窗口的像…...
数据结构--单链表 详解(附代码
目录: 1:链表的概念及结构 2:实现单链表 3:常见疑问 解答 (看到最后!!) 一:链表的概念及结构 1.1 概念: 链表是⼀种 物理存储结构上非连续、非顺序的 存储结…...
leetcode 1749.任意子数组和的绝对值的最大值
思路:dp 说到绝对值,大家肯定不陌生,但是用在dp上就会使问题变得稍微复杂一些了。 我们在最大子数组和的那道题中知道,在状态转移的时候,我们会舍弃掉为负数的连续部分,重新构建连续的子串。但是…...
Linux进程——进程地址空间
前言:在讲完环境变量后,相信大家对Linux有更进一步的认识,而Linux进程概念到这也快接近尾声了,现在我们了解Linux进程中的地址空间! 本篇主要内容: 了解程序地址空间 理解进程地址空间 探究页表和虚拟地址空…...
基于 LlaMA 3 + LangGraph 在windows本地部署大模型 (三)
基于 LlaMA 3 LangGraph 在windows本地部署大模型 (三) 大家继续看 https://lilianweng.github.io/posts/2023-06-23-agent/的文档内容 第二部分:内存 记忆的类型 记忆可以定义为用于获取、存储、保留以及随后检索信息的过程。人脑中有多…...
python3如何安装bs4
在python官网找到beautifulsoup模块的下载页面,点击"downloap"将该模块的安装包下载到本地。 将该安装包解压,然后在打开cmd,并通过cmd进入到该安装包解压后的文件夹目录下。 在该文件目录下输入"python install setup.py&quo…...
docker容器技术篇:rancher管理平台部署kubernetes集群
rancher管理平台部署kubernetes集群 Rancher 是一个 Kubernetes 管理工具,让你能在任何地方和任何提供商上部署和运行集群。 Rancher 可以创建来自 Kubernetes 托管服务提供商的集群,创建节点并安装 Kubernetes,或者导入在任何地方运行的现…...
【计算机网络原理】初识网络原理和一些名词解释
˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱ ʕ̯•͡˔•̯᷅ʔ大家好,我是xiaoxie.希望你看完之后,有不足之处请多多谅解,让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客 本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如…...
车载电子电器架构 —— 关于bus off汇总
车载电子电器架构 —— 关于bus off汇总 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明…...
Linux函数
目录 一、脚本函数 1.1 创建函数 1.2 使用函数 二、函数返回值 2.1 默认的退出状态码 2.2 使用return命令 2.3 使用函数输出 三、在函数中使用变量 3.1 向函数传达参数 3.2 在函数中处理变量 四、数组变量和函数 4.1 向函数中传递数组 4.2 从函数中返回数组 五、函数…...
如何查看centos7中Java在哪些路径下
在 CentOS 7 上,你可以通过几种方式查找安装的 Java 版本及其路径。以下是一些常用的方法: 1. 使用 alternatives 命令 CentOS 使用 alternatives 系统来管理同一命令的多个版本。你可以使用以下命令来查看系统上所有 Java 安装的配置: su…...
信息安全-古典密码学简介
目录 C. D. Shannon: 一、置换密码 二、单表代替密码 ① 加法密码 ② 乘法密码 ③密钥词组代替密码 三、多表代替密码 代数密码 四、古典密码的穷举分析 1、单表代替密码分析 五、古典密码的统计分析 1、密钥词组单表代替密码的统计分析 2、英语的统计规…...
面试题 01.05. 一次编辑
字符串有三种编辑操作:插入一个英文字符、删除一个英文字符或者替换一个英文字符。 给定两个字符串,编写一个函数判定它们是否只需要一次(或者零次)编辑。 示例 1: 输入: first "pale" second "ple" 输出: True示例 2: 输入: first &qu…...
Chapter03-Authentication vulnerabilities
文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...
React Native 开发环境搭建(全平台详解)
React Native 开发环境搭建(全平台详解) 在开始使用 React Native 开发移动应用之前,正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南,涵盖 macOS 和 Windows 平台的配置步骤,如何在 Android 和 iOS…...
3.3.1_1 检错编码(奇偶校验码)
从这节课开始,我们会探讨数据链路层的差错控制功能,差错控制功能的主要目标是要发现并且解决一个帧内部的位错误,我们需要使用特殊的编码技术去发现帧内部的位错误,当我们发现位错误之后,通常来说有两种解决方案。第一…...
深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法
深入浅出:JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中,随机数的生成看似简单,却隐藏着许多玄机。无论是生成密码、加密密钥,还是创建安全令牌,随机数的质量直接关系到系统的安全性。Jav…...
使用分级同态加密防御梯度泄漏
抽象 联邦学习 (FL) 支持跨分布式客户端进行协作模型训练,而无需共享原始数据,这使其成为在互联和自动驾驶汽车 (CAV) 等领域保护隐私的机器学习的一种很有前途的方法。然而,最近的研究表明&…...
在四层代理中还原真实客户端ngx_stream_realip_module
一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡(如 HAProxy、AWS NLB、阿里 SLB)发起上游连接时,将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后,ngx_stream_realip_module 从中提取原始信息…...
相机Camera日志分析之三十一:高通Camx HAL十种流程基础分析关键字汇总(后续持续更新中)
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:有对最普通的场景进行各个日志注释讲解,但相机场景太多,日志差异也巨大。后面将展示各种场景下的日志。 通过notepad++打开场景下的日志,通过下列分类关键字搜索,即可清晰的分析不同场景的相机运行流程差异…...
SpringTask-03.入门案例
一.入门案例 启动类: package com.sky;import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCach…...
tree 树组件大数据卡顿问题优化
问题背景 项目中有用到树组件用来做文件目录,但是由于这个树组件的节点越来越多,导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多,导致的浏览器卡顿,这里很明显就需要用到虚拟列表的技术&…...
CMake控制VS2022项目文件分组
我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...
