游戏引擎学习第254天:重新启用性能分析
运行游戏并尝试让性能分析系统恢复部分功能
我们现在的调试系统这几天基本整理得差不多了,因此我们打算开始清理一些功能,比如目前虽然已经在收集性能分析数据,但我们没有办法查看或有效利用这些信息。今天的计划可能会围绕这方面展开:要么开始真正构建性能分析器,要么尝试一些更实际的整合工作,看看它将如何在实际使用中运行。
我们也可能会处理一下绘制界面方面的问题,因为当前绘制逻辑还比较凌乱,布局也比较混乱,显示效果还不理想,像边框和控件都没画好。
现在我们运行游戏,能看到基础的窗口系统已经可以工作,也可以调整窗口大小。但是我们目前并没有展示任何性能分析数据,而这是我们接下来的重点。
我们希望展示两类性能分析信息:一是内存占用情况,二是耗时分析。其中耗时是最重要的,我们希望能够在调试系统中显示性能分析信息。
我们想要的性能信息主要分为两种:
-
高层次的性能概览:这是一个宏观视角,用来展示每帧大致耗时,包括渲染、模拟、输入处理等各个部分所占用的时间比例。这样可以帮助我们确保每部分运行时间大致正常,不会出现某一部分突然耗时激增的情况。
-
详细的代码性能分析:这是用于代码优化的详细信息。比如当我们发现某段代码运行缓慢时,需要能插入某种标记或调用,以便具体查看那段代码的性能表现。这种分析是为了帮助我们理解并优化特定代码段。
通常这两种信息不会同时需要。我们希望能长期在屏幕上保留一份高层次的性能概览,而当需要优化具体代码时再调用更详细的分析工具。
所以现在我们要做的,大概就是继续构建一个“概览型性能分析器”,类似于传统性能分析器那种列出函数列表、显示函数自身耗时及其子函数耗时、调用次数等信息的方式。这种结构化的展示方式可以让我们快速看出哪部分代码占用时间最多,是一个合理的起点。
此外,传统分析器通常只支持单线程分析,而我们现在的程序是多线程的,因此可能也要考虑用条形图的方式展示不同线程中的事件重叠情况,以便更直观地看到多线程任务的时间关系。
同时我们也注意到当前的 UI 交互还有很多粗糙的地方,比如窗口可以被拖动,但交互手感不好,内容很难看清,缺乏背景遮挡或分隔,使得一些文字在浅色背景下根本看不清。因此也需要加些界面上的遮罩或暗化处理,让调试界面更容易使用。
总的来说,我们现在要解决的是两个问题:一是性能分析系统的构建与可视化,二是界面交互与可读性的提升。从当前情况来看,性能分析是最迫切的问题,所以我们会优先处理这一块,接着再清理其他部分。我们需要回顾之前已经写好的部分,决定接下来要怎样把这套分析系统做好。
game_debug.cpp
:重新熟悉 CollateDebugRecords
函数
当前的调试系统中,我们在 CollateDebugRecords
的代码部分可以看到性能分析的残留结构。在那里,我们记录了每一帧的标记(Frame Marker),并且记录了与这些标记关联的 wall-clock 时间(实际经过的时间),这为后续的性能分析提供了基础数据。
我们还保留了 BEGIN_BLOCK
和 END_BLOCK
这样的事件,它们之间可以配对,以确定代码块的执行时间。系统会查找这些配对的开始与结束事件,并将其构造成时间段(Span)用于分析。不过现在的处理方式还有些问题:
- 目前并没有很好地处理那些跨帧的 block(即在一帧开始,在另一帧结束的代码块),这些情况被忽略了,未来需要加以修复。
- 对于匹配的 block,我们现在只记录了部分特定名称的 block,这是临时做法,不够通用。
在整理这些 debug 记录时,每帧结束都会遍历一次所有事件,然后尝试将它们整理成我们需要的格式。这个整理过程让我们思考是否有更好的方式来构建性能分析结构。
目前对事件的处理方式比较繁琐,尤其是对 BEGIN_BLOCK
和 END_BLOCK
的特殊处理。我们考虑是否可以换个思路,把这些 block 也当作一种“事件”来处理,就像对待普通的数据块一样,而不是单独去匹配成一个 span 区间。也就是说,可以把 begin 和 end block 处理成统一结构,每次记录一个事件,而不是在整理阶段再去做匹配和转换。
这种做法看起来更简单,更统一。每个 block 的记录都会变成一个事件,并关联到某个调试元素(debug element),不再需要在事后分析中单独处理。只要在记录的时候,能明确知道这个事件属于哪个元素,就可以建立起完整的事件流。
这种结构的好处在于:
- 所有事件都可以走同一套系统,降低逻辑复杂度;
- 不需要在事后再花大量逻辑去匹配
BEGIN_BLOCK
和END_BLOCK
; - 可以更灵活地记录事件,不再依赖于是否恰好有匹配的 begin 和 end。
当然,这种方式会加快调试数据的占用,我们会更快地耗尽调试数据的存储空间。但调试阶段并不在意内存消耗,我们完全可以为调试系统分配更大的存储空间,比如 16GB 的内存,足够记录几千帧的详细信息。如果只需要回看几帧,也不会有任何问题。
因此,更倾向于采用这种事件式的统一处理方式,让所有事件都走一样的路径进行存储和回放,把调试系统变得更通用和高效。我们将对 BEGIN_BLOCK
和 END_BLOCK
进行重构,统一到一个记录事件的系统中。
我们目前认为,把 BEGIN_BLOCK
和 END_BLOCK
统一为通用事件来处理是一个不错的想法。虽然还不知道实际实施时是否会暴露出问题,但我们决定先按这个思路尝试一下,后续如果发现问题再做调整。
接下来准备开始清理 StoreEvent
部分的代码,着手进行调整。不过在这个过程中突然遇到一个奇怪的情况——无法切换到编辑模式,表现为按键操作不正常。
具体来说,发现 Shift 键像是被锁定了一样,导致输入状态异常。这种情况以前从未遇到过,无论是在这台机器上还是其他机器上。初步怀疑可能是键盘的问题。
当前使用的是较老的 Das Keyboard 3 型号,以前也曾在另一块 Das Keyboard 上遇到过类似“按键卡住”的问题。而其他地方都已经更换成 Razer 键盘,只是还没给这台开发机配一把新的。考虑到以往的经历,这次的问题可能也是这块旧键盘本身存在物理故障或者是接触问题。
虽然这是个小插曲,但还是提醒了我们旧设备可能带来的问题,后续有必要更换这块键盘以避免干扰正常开发流程。
game_debug.cpp
:简化 CollateDebugRecords
并修改传递给 StoreEvent
的内容
我们决定将 BEGIN_BLOCK
和 END_BLOCK
事件直接存储到对应的元素中,不再进行之前复杂的匹配和处理。每当遇到一个 BEGIN_BLOCK
或 END_BLOCK
,就把这个事件存储到其对应的 debug 元素中。由于这些事件在创建元素时就关联在一起,因此可以简化处理逻辑,不再关心 frame index 是否匹配,只要配对关系成立,就进行存储。
这样,每个元素中就会有一个以“进入-退出-进入-退出”的模式组织的事件链表,代表了该代码块在运行过程中的每一次进入与退出。这个结构能够完整记录每次调用的时机和持续时间。
不过,这种方式虽然能记录“某段代码被调用了多少次,每次持续了多久”,但仍存在一个问题:我们并不知道这些调用是被谁触发的。换句话说,缺少了调用关系的信息。要知道调用关系,我们只能依赖事件的时间顺序进行推断,比如通过分析时间戳判断哪个事件嵌套在另一个事件内部,但这种方式不够直接,也容易出错。
为了解决这个问题,我们考虑在存储事件的时候,加入一个额外字段,用来记录“当前事件的父级元素”,即这个代码块是被哪个上层代码块调用的。这样就能在分析调用关系时更加明确,能够清晰地追踪每个调用链上的父子层级。
简而言之,我们的思路是:
- 简化事件处理流程,遇到
BEGIN_BLOCK
和END_BLOCK
就立即记录,不再依赖复杂匹配。 - 每个元素维护一组“进入-退出”的事件序列,完整还原每次执行时长。
- 为每个事件记录其“父级调用者”,明确调用关系,方便之后构建调用树或进行性能分析。
- 删除原有多余的逻辑和临时代码,进一步简化结构。
这样处理之后,性能分析数据将更加结构化、可视化效果也更容易做出明确的父子层级展示,同时提升了系统的灵活性和可维护性。
黑板:调用归因(Call Attribution)
我们设计了一个用于调试的结构体(debug element),这个结构体记录了每一次函数调用的信息。我们能够掌握的信息包括:
- 每个函数被调用的次数;
- 每次调用持续的时间;
- 每次调用发生在哪一帧(frame)上。
这些数据都存储在一个类似链表的结构中,我们已经具备了相应的采样记录能力。
不过,目前仍有一个重要的信息缺失:调用者是谁。例如,函数 foo
可能既被函数 bar
调用,也可能被函数 boz
调用。如果我们在调试过程中发现 foo
被调用了多次,并且我们已经记录了调用的时间和帧位置,但我们无法区分哪一次调用是来自 bar
,哪一次是来自 boz
。这对于后续分析调用关系、定位性能瓶颈会造成不便。
为了能够获得更精细的调用上下文分析结果,比如“函数 bar
导致了 foo
花费了多少时间”、“函数 boz
又占用了多少”,我们希望在调试信息中增加一个额外的指针或引用,用来指向调用该函数的上层函数对应的 debug element。这样我们就能从 foo
的调用记录中追溯到具体的调用来源。
因此,建议在事件存储结构中,加入一个额外的指针字段,用来标记“谁”调用了这个函数调用。这种方式在实际使用中会提供更高的灵活性和更丰富的分析能力,比如可以选择性地启用或禁用这部分引用信息,视具体分析需求而定。
这类优化将在调试和性能分析工具(如 game profiling 系统)中提供更高质量的追踪数据。我们可以进一步查看事件存储结构的定义(如 game 的 StoredEvent
)来确认该指针的可添加位置。
game_debug.h
:记下将调用归因数据存储到 debug_stored_event
的计划
在当前的事件记录机制中,我们保存了事件的帧索引和事件的类型。但如果能额外存储一个信息,例如事件的“父级调用者”,会对后续分析非常有帮助。
虽然我们暂时不需要立即添加这个信息,但我们意识到这是一个重要的点,因为我们只在当前的相关性分析过程中才能知道每个事件的调用者是谁,也就是说,只有在执行事件匹配遍历(correlation pass)的时候,才能知道一个事件是被谁调用的。
这种信息之所以只能在特定阶段获得,是因为我们是通过遍历事件数组并按照顺序配对“打开”和“关闭”事件,推断出哪个事件是谁的父级。在这个阶段我们掌握了所有调用关系。但这个数组在分析完成之后就会被释放,也就是说,绘制图形或回放调试信息的时候,我们再也无法获得这些调用链信息。因为原始事件流不再保留,我们也没有记录每个事件的父级信息。
因此,如果我们希望后期还原调用关系,就需要在事件存储结构中预先记录好父级指针,否则信息就会丢失。
也考虑过一种替代方案:直接写入一个持续增长的事件流,不做任何中间结构保存,而是通过向后查找的方式追踪调用者。但这种方式可能效率较低,尤其是在处理大体量事件时会产生很高的性能开销。因此,引入中间结构并定期进行处理,是为了在性能和可用性之间取得平衡。
总体来说,当前的设计思路是:仅处理当前帧的新事件,避免每次绘制或分析都去遍历整个事件数组。这样既节省计算资源,又可以保留需要的重要数据。最终目标是减少处理开销,同时保留必要的调试信息。我们所做的权衡,是在保留调试所需信息的前提下,尽量减少无效数据扫描和处理的成本。
game_debug.cpp
:实现调用归因
我们开始重新编译代码,并准备进入分析和实现性能分析(profiling)相关功能的阶段。接下来的目标是让绘制调试菜单中的性能分析部分(profile UI)正常工作。
在完成事件归并(collation)之后,绘制完成各个调试元素(debug elements)之后,我们会进入主调试菜单的绘制过程。在这一阶段,我们会调用 DebugDrawMainMenu
相关的函数,并在内部进一步处理性能分析的绘制工作。关键点在于:我们开始进入实际将性能分析信息可视化的流程。
调试系统中包含多种类型的元素,其中之一是 CounterThreadList
,这是我们想要首先处理的部分。我们开始分析它的作用和使用方式。
接下来查看了与 CounterThreadList
相关的结构定义和调用位置。我们注意到在某些位置它被用于打印信息,但目前我们还不完全理解这些结构的具体意义。因此,暂时将相关部分注释掉,以便聚焦于理解其实际作用和分析在哪些位置被使用。
当前已知的是:
CounterThreadList
和CounterFunctionList
都是性能分析中用于统计信息的数据结构;- 它们可能被用于绘制与线程或函数调用相关的性能图表或统计视图;
- 在实际绘制逻辑中,它们作为调试元素的一部分存在,在调试菜单中被绘制出来;
- 我们将逐步理清它们的结构、用途以及如何通过它们来实现可视化功能。
目前的策略是:从分析调用路径和绘制流程开始,逐步实现或修复 CounterThreadList
等数据的可视化输出,为后续的性能分析提供可靠的展示工具。通过理解其用途和代码流转方式,为调试系统构建更完善的分析界面奠定基础。
game_debug.cpp
:将 DEBUGDrawElement
和 DEBUGDrawEvent
合并为一个函数
目前代码中存在两个看起来类似的函数:DebugDrawElement
和 DebugDrawEvent
。回顾后发现,它们实际上没有明显区别,其存在只是由于早期对如何划分状态存储方式还存在疑问而做出的结构安排。
随着代码演进,这种分离已不再必要,因此决定将两个函数合并成一个统一的函数,即只保留 DebugDrawElement
。原来的 DebugDrawEvent
中的逻辑和代码已经与 DebugDrawElement
几乎完全一致,没有保留的意义。
接下来,移除了多余的函数定义,并将可能遗留的逻辑暂时整合到当前保留的函数中,以防后续还需要参考。
在新保留的 DebugDrawElement
函数中,实际上只依赖一个内容:当前正在绘制的 StoredEvent
(也就是存储的调试事件)。原来还传递了一个 view
参数用于视图计算,但从逻辑看出,这个参数并未真正使用,因此也一并省略,简化了函数签名和使用方式。
最后,调整函数中处理 element
的调用方式,使逻辑更加直观一致。将原来分散的事件绘制逻辑提取、合并,保留核心操作,并确认调用的是最近一个事件的数据作为绘制目标。
总体调整结果如下:
- 合并
DebugDrawElement
和DebugDrawEvent
,只保留前者; - 移除冗余代码和无效参数(如视图);
- 明确绘制依赖的数据结构为最近的
StoredEvent
; - 统一命名和调用方式,减少重复,简化整体结构。
这次整理让绘制调试元素的流程更加清晰,也为后续功能拓展或维护降低了复杂度。
黑板:草拟概览性能分析器
当前的任务是规划我们希望构建的性能分析(profile)视图类型。我们决定从最基本的一种开始——概览视图(overview),这也是之前已经实现过的一种形式。
这类概览视图的目标是以线程为单位,展示在一段时间内各线程的运行活动。可视化方式是以“线程”作为纵向的分类(垂直方向每一行代表一个线程),而以“时间”作为横向的刻度(从左到右表示时间流逝)。在每个线程的时间线上,绘制不同函数的执行区间,通过方块或条形的方式展示。
具体形式如下:
- 纵向表示不同线程(lanes);
- 横向表示时间流逝(clock ticks);
- 在每个线程时间线上,绘制函数调用的执行区间,显示函数名;
- 基于事件中的 open/close 块数据来确定函数的执行时间段,并以此绘制可视化块。
目前我们在数据结构上还没有一个完全的“层次结构”(hierarchy)来呈现调用关系,这使得要构建一个明确的调用图(flow graph)仍有一定难度。因此,在开始绘制前,我们意识到也许可以在可视化过程中临时构建某种形式的调用结构或数据索引,以便更有效地布局和展示各个函数调用。
此外,随着实际实现推进,我们也会逐步明确更多细节,包括:
- 如何更清晰地区分线程和函数的显示区域;
- 是否需要对函数调用深度做视觉上的嵌套处理;
- 如何提升渲染性能,避免处理庞大的事件数组时出现效率问题。
这套概览视图的实现将作为性能分析系统的基础,为之后更复杂的视图(比如调用图、按函数分类统计等)提供出发点。我们计划从这个最直接、最易理解的展示方式入手,一步步建立起完整的性能可视化工具体系。
game_debug_interface.h
:开始实现 ThreadIntervalGraph
当前的目标是为性能分析系统实现一种新的可视化类型,我们将其命名为 线程区间图(Thread Interval Graph),它专注于展示各线程在一段时间内执行函数的区间分布。
我们定义这种视图类型为一种“更深层”的视图,用于更细致地查看线程中的函数执行情况。设计的核心思路如下:
线程区间图的基本理念
- 每个线程有一条独立的时间线;
- 在线上绘制函数调用区间(使用开始时间和结束时间);
- 显示函数名称;
- 横轴为时间刻度;
- 纵轴为线程分布;
- 支持筛选,仅显示特定函数的调用区间(如果传入特定函数名);
- 如果传入空字符串,则显示所有函数。
实现规划
我们在现有的 debug_profile
模块中加入这个新的视图类型:
- 定义新的视图类型名为
DebugView_ThreadIntervalGraph
; - 注册该类型,使其能够在调试界面中被调用;
- 提供函数名参数:作为过滤器,决定是否只显示某一个函数;
- 在调试菜单中,默认只显示
game_update
和render
等游戏逻辑代码,而非平台层逻辑; - 将这个视图作为一个基础结构添加进调试系统,使得用户可以选择它并查看每帧中不同线程的函数执行情况;
- 可以考虑在视图中显示每个执行块的句柄信息或 open block 数据,便于追踪来源。
关于部分旧代码的处理
之前存在一些与函数列表、计数器等有关的调试逻辑,但这些代码目前不符合新的需求,也不再适用,因此可以忽略或清理。同时,对于这部分线程区间图的核心绘制逻辑,目前还未开始正式实现,但已经完成了结构的初步搭建,为后续图形绘制和逻辑遍历做好了准备。
当前阶段目标总结
- 明确了我们要实现一种新的可视化图;
- 将其纳入已有的调试系统框架中;
- 提供了参数支持(函数名过滤);
- 定位其适用范围(主要关注游戏逻辑线程);
- 清理了无关代码,为后续实现留出干净入口。
接下来的工作将聚焦于如何利用记录的函数调用事件,绘制出清晰的、可交互的线程区间图形界面。
(void) 是消除一些clangd 的警告
运行游戏并看到 END_BLOCK
被打印出来β
段错误吗?
当前我们已经移除了冗余的函数,使代码结构变得更清晰,这是一个积极的进展。接下来我们观察到了调试系统中一些异常行为,并对其进行了初步分析。
当前调试行为及异常现象
- 我们注意到调试信息中正在打印
endblock
数据块,这引起了我们的注意; - 初步判断这可能是由于我们在调用
store_event
的时候自动触发的行为; - 然而目前这块数据的打印看起来十分奇怪,有些抖动或跳变现象;
- 在调试暂停状态下,系统本不应该更新,但依然发生了刷新行为,这非常异常,推测可能是某处逻辑未正确检测暂停状态;
- 这一行为非常可疑,需要后续进一步调查其根本原因。
内存区域耗尽测试
- 此外,我们有意识地让内存分配区域(arena)耗尽以观察系统的反应;
- 系统提示大约还有 30 秒内存空间,随后逐步减少至 5MB、4MB……最终耗尽;
- 在耗尽时我们特别留意系统是否能正常处理该情况;
- 最终在内存耗尽时出现了某些“奇怪的数据”或行为(可能是图像/调试数据显示异常),这同样是值得关注的问题;
- 除此之外,系统大体上在其他方面保持了正常运行。
总结与下一步方向
- 成功移除了不必要的函数,提高了整体代码可读性;
- 发现
store_event
可能引发了不希望发生的数据输出行为; - 注意到调试系统在暂停状态下依然有“跳变”或“闪动”,违反预期;
- 内存区域耗尽时,系统虽未崩溃,但出现了可疑的数据图像,应进一步观察内存管理与调试渲染逻辑;
- 下一步需要集中精力定位调试系统在暂停状态下仍更新的问题,并进一步分析 arena 耗尽后的容错机制是否健壮。
这些观察结果为调试系统的健壮性提供了宝贵信息,有助于后续优化和稳定性提升。
game_debug.cpp
:注释掉 StoreEvent
的调用,以确认 32MiB 的内存不足
我们对当前的调试系统进行了一些内存相关的测试,并得出了几个关键结论和后续改进方向:
内存占用测试及现象分析
- 使用
begin_block
和end_block
进行测试,初始情况下不存储事件时系统运行良好; - 只存储其中一个事件时,内存占用减半,依然表现正常;
- 同时存储两个事件后内存开始迅速耗尽;
- 初始设置的 32MB 调试存储空间完全不足以容纳当前记录的事件数量,内存被快速“吃掉”;
- 当只存储一个事件时运行正常,说明系统逻辑本身没有问题,只是事件数量太多;
- 临时将调试存储空间设置为一个更大的值后,系统能够稳定记录完整的一帧数据;
- 说明只要内存充足,系统的事件记录机制是有效的;
对事件量的初步判断与优化方向
- 当前调试系统在每帧记录的事件数量过多,意味着某些地方设置了过多的区域(zone)或嵌套;
- 尽管增大内存可以缓解问题,但并不能从根本上解决事件过多的问题;
- 可能需要对记录策略进行优化,例如合并重复的事件、限制最大嵌套层级、仅记录关键事件等;
- 系统虽然能记录下完整帧,但每次刷新内存的速度极快,依旧不够理想,后续需做进一步内存优化设计;
调试层级显示的修正需求
- 当前的调试层级(hierarchy)显示中出现了一些不应当出现在其中的元素;
- 推测是某些调试元素(debug elements)被错误地加入到了层级结构中;
- 为了更好地组织视图,需要将这类不应出现在主层级中的元素排除出去;
- 后续需要调整处理逻辑,区分哪些是用于显示层级的核心事件,哪些是辅助性事件或不应显示的内部记录项;
- 这将改善调试视图的清晰度和结构逻辑,使调试工具更易于阅读和分析。
总结
- 当前调试系统基本运行稳定,但内存使用率极高;
- 临时扩展内存缓解问题,但需从源头优化事件记录策略;
- 调试显示结构存在错误归类问题,需修正层级渲染逻辑;
- 整体机制可用,后续工作重心在于压缩事件数据、提升可读性、控制内存消耗。
奇怪
为什么只显示这一行
game_debug.cpp
:修改 GetElementFromEvent
,加入参数 b32 CreateHierarchy
,以便根据条件调用 GetGroupForHierarchicalName
我们当前遇到的问题是在处理调试事件时,有一类特殊的元素(Hierarchy类型的 debug element)不应当参与正常的层级结构分组处理。它们应被统一归入一个固定的分组中,而不是通过一般的层级规则进行归类。为了解决这个问题,我们需要对事件解析和分组逻辑进行调整。
当前问题描述
- 有一批特定的调试事件不应参与层级结构的归类;
- 这类事件应始终被归入一个固定的父组,而不是根据名称层级进行分组;
- 当前
get_element_from_event
函数会默认通过get_group_for_hierarchical_name
来查找或创建层级分组; - 这就导致了我们不希望出现的自动归类行为;
解决思路与设计调整
我们可以在 get_element_from_event
函数中引入一个额外的参数或机制来控制是否跳过层级结构的查找过程:
新增标志控制行为
- 引入一个
add_directly_to_parent
的布尔标志,用于控制是否直接将该事件归入指定父组; - 当该标志为
true
时,跳过get_group_for_hierarchical_name
的调用,直接使用提供的parent_group
; - 否则,执行原有的层级结构归类逻辑;
函数内部逻辑变更示例
if (add_directly_to_parent) {// 直接使用传入的 parent,不进行层级归类assign_to_group(parent_group);
} else {// 按照名称层级查找或创建分组group = get_group_for_hierarchical_name(name);assign_to_group(group);
}
理由与好处
- 避免了非层级元素被错误地纳入分组结构;
- 允许更灵活地控制调试元素的归类行为;
- 保留现有层级分组逻辑的完整性;
- 简化了这些特殊元素的调试显示逻辑;
未来可优化方向
- 可以进一步将事件分为普通层级事件与特殊事件两个通路处理;
- 根据事件类型自动决定是否走“扁平化分组”逻辑;
- 让调试系统本身具有更清晰的分类能力,以减少每次手动判断与传参的负担;
小结
我们现在通过引入一个控制标志 add_directly_to_parent
,成功区分了哪些事件需要参与层级结构,哪些事件应统一归为某一组。这样一来,调试显示逻辑就更加清晰且可控,避免不必要的混乱结构,同时为后续调试系统的维护和优化打下了良好的基础。
game_debug.cpp
:引入 ProfileGroup
的概念
我们现在的目标是让某些调试事件(例如 Hierarchy类型的调试元素)不被加入层级结构中,而是直接存入一个专门的“分析块容器”中,以便更好地管理和展示这些不需要参与常规层级结构的调试数据。
当前处理逻辑概述
- 在调用
get_element_from_event
函数时,我们决定通过传入参数控制是否允许该元素进入层级结构; - 对于某些不希望层级化的事件(如 Hierarchy类型),我们在调用时将
create_hierarchy
参数设置为false
; - 这类元素会被统一放入一个专门的容器,例如
profile_group
,这个容器位于顶层节点profile_root
之下; profile_group
是一个我们新增的变量,需要将其加入调试状态结构中(debug_state
);- 目前因为函数
get_element_from_event
被多重声明(重载),而create_hierarchy
参数只出现在其中一个定义中,导致调用时不明确,编译器报出调用歧义错误; - 进一步发现问题是因为我们在头文件中对
get_element_from_event
的声明不完整,缺少了新的参数,需要更新声明以匹配实现;
操作细节与调整
- 将
get_element_from_event
函数增加的新参数create_hierarchy
正确声明在头文件中,消除重载歧义; - 确保只有一处函数声明,并与实际实现一致;
- 在
debug_state
中添加新的profile_group
字段,用于保存这类特殊事件的容器; - 在创建变量组时(
create_variable_group
),指定对应的大小为7
或8
,并绑定到新的容器中; - 编译后检查
get_element_from_event
调用是否正确解析,是否成功地将事件放入非层级化结构中;
整体架构演变意义
- 实现了调试系统中“层级事件”和“非层级事件”的并存机制;
- 提高事件展示与管理的灵活性;
- 通过简单标志参数控制是否参与层级化归类,逻辑清晰、易于维护;
- 建立了专属的
profile_group
容器,未来可以扩展更多仅限于该组的展示和处理方式;
小结
通过对 get_element_from_event
函数添加控制参数并修正声明,同时引入新的分析容器 profile_group
,我们成功实现了将特定调试事件排除在默认层级结构之外的逻辑。这一改进提升了调试工具对不同类型事件的表达能力,为后续的可视化和调试体验打下良好基础。
运行游戏并查看调试可视化效果
我们现在的处理逻辑是:虽然仍然在记录所有调试信息,但这些信息已经被存储到了一个不可见的“后台区域”中——这是我们想要的效果。
当前目标与思路
我们想要从这个专门的调试组中,将数据提取出来并以图形方式绘制出来。为此,我们可以:
- 利用已有的
profile
数据结构,其中包含线程相关的事件信息; - 通过访问这个结构内的组成员,遍历出所有我们感兴趣的事件;
- 将这些事件绘制出来,构建可视化的线程时间图(thread interval graph);
- 虽然当前系统已经具备这些数据,但还没有实际绘图,我们现在准备实现这一步;
技术细节分析
thread interval graph
是我们用来可视化线程运行区段的模块;- 每个线程拥有一组事件,我们通过遍历
element group
中的成员来获取这些事件; - 需要知道当前的帧索引,以便绘制“最新的一帧”;
- 推测中,我们应该在调试存储结构中已经存储了最新帧的索引,例如通过
most_recent_frame
这样的字段; - 可以通过
most_recent_frame.index
之类的方式获取正确的时间轴基准; - 遇到了一个变量
frame_bar_scale
,似乎是早期移植代码时残留的,可能原意是计算显示缩放比例或线程总数,但现在看起来已经无效,因此可以忽略或清除;
小结
- 所有调试信息现在已经按照预期进入专门的组中;
- 下一步可以在
thread interval graph
中遍历这些事件并开始绘图; - 需要从调试状态中提取“当前帧”的索引,用于绘制最近一次的线程活动;
- 遇到的
frame_bar_scale
被识别为废弃变量,不再具有实际意义; - 接下来我们将基于已组织好的调试数据,实现更直观的线程区段可视化图形。
这意味着整体框架已经打通,后续重点放在如何渲染可视化图形上。
game_debug.cpp
:让 DrawProfileIn
绘制一帧
我们当前的任务是将性能分析系统中记录的数据以图形方式绘制出来,重点是以帧为单位进行绘制。目前我们采取的是从最新的帧中提取时间块数据进行可视化的初步尝试。以下是详细总结:
目标与思路梳理
- 当前的渲染逻辑中,已经实现了性能分析数据的记录和存储,但绘制部分仍未完成;
- 我们决定只绘制最新的一帧,而不是遍历所有帧,因为绘制全部帧过于耗资源,屏幕空间也不足;
- 将通过“存储事件”中的“打开块(begin block)”和“关闭块(end block)”来计算时间区间,进行绘制;
- 时间区间对应屏幕上的 X 轴坐标,线程对应 Y 轴坐标(每个线程对应一条“lane”)。
数据结构与绘图逻辑
帧选择与时间跨度计算
-
使用“最新帧”进行绘制,具体是
most_recent_frame
; -
确认“帧标记”是在调试数据绘制后执行,因此
most_recent_frame
是已完成记录的一帧; -
获取该帧的时间跨度为:
frame_span = frame_end_clock - frame_begin_clock
-
计算屏幕绘图的缩放比例(frame_scale):
frame_scale = pixel_span / frame_span
即将时间范围标准化后映射到绘图区域的宽度上。
线程信息处理与图形绘制
线程区间绘制
-
从
profile_group
中遍历所有事件(stored_event
); -
判断每个事件是否属于当前帧(根据其帧索引判断);
-
对于成对出现的
begin_block
和end_block
:-
记录下
begin_block
,当遇到对应的end_block
时使用两者之间的时间跨度绘制矩形; -
位置计算如下:
min_x = profile_rect.min_x + scale * (open_event.clock - frame.begin_clock) max_x = profile_rect.min_x + scale * (close_event.clock - frame.begin_clock)
-
-
Y 轴位置根据线程在 lane 中的位置进行分配,使用:
lane_height = profile_rect.height / lane_count
然后:
min_y = profile_rect.max_y - (lane_index + 1) * lane_height max_y = profile_rect.max_y - lane_index * lane_height
问题点与后续计划
- 当前存储事件是单向链表(从 oldest 到 most_recent),无法从尾部逆向遍历;
- 可能需要改成双向链表,或者以后考虑用树形结构来优化事件组织;
- 目前线程是用系统 ThreadID 表示的,不方便进行稳定排序和索引,需要将其替换为内部维护的“序号化 ID”;
- 图形绘制采用 push_rect 的方式将矩形压入渲染组,使用的是
no_transform
; - 目前省略了文本绘制(如函数名标签),后续再实现;
- 无用的变量(如 frame_bar_scale)已清理,保持绘图逻辑简洁清晰;
总体结构现状与后续工作
-
当前绘制逻辑已经可运行,并且可以从最新帧中准确提取和渲染函数块时间;
-
线程图将以条形区域方式直观显示每个线程的执行片段;
-
后续工作包括:
- 优化线程 ID 管理;
- 改进事件链表结构;
- 添加文本信息(函数名称等);
- 支持多帧回顾或缩放;
- 提高绘制性能和美观性。
整个系统已经完成从数据记录到图形初步绘制的闭环,未来将聚焦在可用性提升与功能细化上。
奇怪怎么没有呢
屏幕打开profile 没进来
DebugType_EndBlock 中StoreEvent被屏蔽掉了
再次运行
world 里面显示会有bug,
为什么上面树折叠对下面有影响
运行游戏并查看正确的调试可视化ε
现在的效果非常理想,甚至可以说出乎意料地正确。
虽然目前还没有任何机制确保绘制的顺序是准确的,但初步结果已经达到了可接受的状态。接下来仍然有许多工作要做,尤其是排序顺序的问题还没有解决。当前的绘制过程缺乏对调用层级或先后关系的掌握,导致无法准确堆叠调用块,显示出正确的嵌套或包含关系。
这也体现了性能分析器中处理调用关系的一大难点:没有现成的信息说明哪个函数是谁的调用者或被调用者。因此,要正确堆叠这些时间块并在图表中呈现函数嵌套结构,是一个比较复杂的问题。
尽管如此,整个绘制系统的基础框架已经逐步搭建完成,进展令人满意。我们正在一步步接近理想状态。后续将重点解决排序与堆叠的逻辑,确保最终展示的数据既准确又具有层次感。整体来看,性能分析的可视化已经迈出了关键的一步。
Q&A
game_debug.cpp
:将 PointerToUint32
转换为 CloseEvent->GUID
我们考虑到,如果拿到一个指针后,希望将其转换成可以直接使用的形式,比如某种更简化、更适合处理的类型(例如一个索引或偏移量),这一步操作必须在某个明确的位置进行,也就是说,它必须在某个阶段完成这个转换过程。
这是在处理数据结构或事件记录时非常关键的一步,尤其是在可视化或者调试系统中,当我们遍历和渲染各种数据时,如果依赖于指针本身,那么在渲染或比较时将非常不方便。因此,必须将指针或引用的信息提取出可以计算和比较的值(比如将其转换为编号、序号、数组索引等)。
这类转换通常要保证:
- 统一性:所有指针都应该以相同规则转换成可处理形式;
- 安全性:转换之后不应丢失必要的语义信息,例如不能转换得太粗糙以至于无法区分不同的元素;
- 效率:转换过程应尽量靠近数据采集或数据初次记录的位置完成,避免每次使用时重复处理。
因此,这种“指针向可用形式转换”的操作必须出现在一个明确而恰当的阶段,以保证后续渲染、比较、排序等处理逻辑可以顺利进行。
#define PointerToUint32(Pointer) ((uint32)(uintptr_t)(Pointer))
uintptr_t 能保存一个指针
#define PointerToUint32(Pointer) ((uint32)(uintptr_t)(Pointer))
这段宏定义的作用是将一个指针类型安全地转换为一个 uint32
类型的整数。
为什么要用 uintptr_t
?
uintptr_t
是 C/C++ 标准库 <stdint.h>
或 <cstdint>
中定义的一个 无符号整数类型,它的关键特性是:
它保证足够大,能够安全地存储任意指针的值。
换句话说,它是专门用来存放指针数值(即地址)的整数类型。
分析这段宏的转换流程:
(uint32)(uintptr_t)(Pointer)
-
(uintptr_t)(Pointer)
这一步把Pointer
(原本是一个指针类型,比如void*
)转换成uintptr_t
,也就是把指针地址转换成一个整数值。这样可以安全获得指针的“地址数值”。 -
(uint32)
然后再将这个整数地址强制转换为uint32
,也就是取这个地址的低 32 位。
为什么不能直接 (uint32)(Pointer)
?
直接这么写是不安全的,原因有:
- 在某些平台(尤其是 64 位系统)上,指针是 64 位的,而
uint32
只有 32 位。直接强制转换会丢掉高 32 位,编译器可能会发出警告或报错。 - 不同架构对指针到整数的直接转换有不同的要求。使用
uintptr_t
是标准定义的方式,可以确保转换的安全性和跨平台兼容性。
总结
使用 uintptr_t
的原因是为了:
- 安全性:明确表示我们是要把指针的地址转成整数。
- 可移植性:保证在不同平台上(如 32 位/64 位系统)都能正确处理指针到整数的转换。
- 避免编译警告或错误:防止类型系统因为不安全的强转报错。
如果你在做类似 ID 映射、调试可视化、缓存索引等,需要把指针“压缩”成一个整型值的情形,这种做法是标准且推荐的方式。需要注意的是:最终的 uint32
可能会导致截断,如果系统是 64 位的,多个不同的指针可能映射成相同的 uint32
值,这在设计上需要考虑。
越界了
问:你觉得用“调用树”来追踪多线程之间的依赖关系怎么样?
我们在考虑使用调用树来追踪多线程中的依赖关系时,其实面临一些困难。如果我们以性能分析器为背景来讨论,会发现传统的调用树并不能真正帮到我们。
我们真正需要了解的是——每一组调用是如何与其“父级调用”关联的。这一点非常复杂,因为普通调用树或调用图只能提供“函数A调用函数B”这样的信息,但它不能告诉我们:在特定的一次调用过程中,这一组调用链是如何形成的,尤其在多线程环境下更难追踪。
我们可能需要引入更精细的记录方式,比如为每一次函数调用都记录它的“父调用”的返回信息,甚至为每个调用都维护一条具体的路径。这种方式虽然看起来像调用树,但其实远远超过了传统调用树的能力。
传统的调用图仅仅表示“函数A有时会调用函数B”,但这种结构并不能区分不同线程或不同时间片中,哪一次调用具体属于哪个调用上下文。我们要做的更像是一种实时的调用链追踪,记录每一个函数调用的完整来源和上下文,而不是静态的、抽象的函数依赖关系图。
我们考虑引入某种机制,以精确地记录每一个函数调用的发生过程和其所属的父调用,这样才能在多线程场景下真正还原调用链路、分析依赖,并用于性能分析或调试。
问:你用什么命令从过场切换到游戏?
我们在游戏中切换操作模式时,使用的是空格键(Space Bar)。当需要从“切割”模式切换出来或进入其他状态时,就按下空格键来实现这个转换。这个按键被用作模式切换的快捷方式,方便我们在不同操作之间快速过渡。整个过程依赖键盘输入,通过监听空格键来触发对应的状态切换逻辑,从而改变当前的交互方式或行为。
问:你怎么处理在多线程中运行但计算时间超过一帧的任务?比如我听说新《极限竞速》的后视镜更新频率只有主画面的一半
我们在处理运行于独立线程的任务时,即使它们的执行时间超过一个帧的计算周期,也可以很好地应对。这种情况完全没有问题。当前我们暂时不会对这些任务进行绘制,但一旦整体流程整理完毕,就能轻松地添加这部分功能。
具体处理方式也不复杂。我们可以通过事件的开启与关闭来判断跨帧任务的存在。例如,如果当前帧中没有检测到“开启事件”,但却发现了“关闭事件”,这说明该任务其实是从前一帧就已经开始运行了。在这种情况下,我们只需从当前帧的起点开始绘制即可。
为此,我们可以将其绘图的时间偏移量直接设置为零(即表示从该帧的起始位置开始),这样就能正确反映出任务的持续时间,即便其开始时间早于当前帧。这个处理策略可以确保跨帧任务也能被准确地可视化,保证数据的完整性与时间线的连续性。
问:为什么没人告诉我游戏里有克兰普斯?
我们一开始居然没有被告知这个游戏里居然有克拉帕斯(Krampus),实在令人惊讶。其实很早以前我们就已经收到过克拉帕斯的图片,甚至可能在他首次出现在直播前,我们的邮箱里就已经有相关内容了。也许有人没注意到,但我们记得自己确实收到了关于克拉帕斯的信息,他早就已经在游戏中了。
更重要的是,克拉帕斯不仅仅是游戏中的一个角色,他几乎是整个游戏存在的核心动因。因为圣诞老人本质上只是个自负又冷漠的家伙,完全不能理解一个没有手的小男孩怎么可能满足于每年只收到几顶帽子。他需要的是能在森林中冒险、行动的能力,而不是装饰品。
而克拉帕斯出现之后,情况完全改变了。他理解这个孩子的真正需求,并且作为那个众所周知的“会肢解小孩”的存在,克拉帕斯自然在他的袋子里带了很多手。于是他将一只手送给了小男孩,让他重新拥有行动能力。从这个角度看,克拉帕斯拯救了一切。
这也构成了整个故事的主旨:克拉帕斯并不是恐怖的怪物,而是真正帮助孩子的英雄。我们从这个设定中得出结论,他在游戏中所扮演的角色不仅有趣,甚至是富有象征意义的核心存在。
问:一边写代码一边和观众聊天对你有帮助吗?
一边编码一边说话其实并不会带来什么帮助,反而会让编程变得非常困难。我们经常发现,在尝试处理复杂内容的时候,同时进行语言表达会干扰思路,导致效率下降。这种情况下,我们很难专注于逻辑推理或者细致的实现细节。
尽管如此,我们还是尽力而为,在保证思维不被完全打断的情况下去表达正在进行的操作或者解释思路。但总体而言,同时说话和编码确实是种挑战,需要在精力分配上不断调整。
问:有没有考虑用火焰图来可视化性能数据?
我们确实没有考虑使用 flame graph(火焰图)来进行时间可视化,原因有很多。首先,目前我们刚刚开始进行性能分析的可视化,像 flame graph 这样相对复杂的工具还需要编写大量的额外代码,特别是对于一个内部的分析系统来说,这种投入并不划算。
虽然以后理论上可以实现 flame graph,但我们并没有太大意愿去做,因为我们并不真正需要它。我们已经在调试系统上投入了不少时间,更希望把精力放在系统架构本身,让整个设计能清晰地展现出权衡和决策的过程,而不是把时间花在制作一些“花哨”的图表上。
flame graph 本身在信息传达上其实并没有太多独特优势,很多时候从更简单的图形中就能获取我们需要的信息。除非是在某种特别混乱、结构极其复杂的代码库中,我们才可能真的需要 flame graph 来协助理解代码执行路径。但在我们的情况下,它并不属于一个高优先级的工具。
因此,我们倾向于使用更直接、更轻量的可视化方式来辅助性能分析,避免过度投入在看起来好看但实用性不高的工具上。总体而言,flame graph 并非必须。
问:这是不是软件渲染?帧时间太高了
当前帧时间变卡顿的原因并不是运算逻辑本身在运行,而是我们正在处理数量非常庞大的调试元素。由于我们记录了任意数量的帧,每一帧中包含大量的事件数据,现在每次都必须遍历所有帧、每一个事件进行处理,这种做法代价极高。
目前的系统中,已经积累了成百上千甚至上百万条调试记录,而我们仍在使用一个简单的线性遍历方式去查询和处理它们。每一帧都需要重新完整地遍历所有数据,这种方式显然在数据量较大时无法承受,会导致性能急剧下降,尤其是在持续记录的场景下尤为严重。
因此,现有方式已经不适合继续使用,我们必须寻找一种更高效的查询机制来获取所需信息。必须优化这部分逻辑,例如通过构建更高效的数据结构,缓存机制或索引来快速定位目标数据,减少不必要的重复计算。换句话说,我们需要一种性能更高、响应更快的方式来支撑分析和绘制工作。
game_debug.cpp
:停止每次都循环遍历所有事件
目前系统的性能瓶颈基本上出现在一个特定的 if
判断中。如果我们将这个判断逻辑去掉,性能问题就会得到缓解,看起来一切就能正常运行。然而,代码并没有正确地重新加载,原因在于有一段代码被标记为“无法到达”,这导致编译器并未真正构建新的版本。
在我们手动绕过这部分判断之后,程序确实恢复了正常运行,但这也揭示了一个潜在问题 —— 我们当前对字符串的处理存在严重缺陷。虽然逻辑上是将字符串压入某个容器或栈中,但实际上某些字符串并没有成功地被推入进去。
这一现象在代码热重载时表现得尤为明显。本应被加载的字符串在热重载后丢失了,说明它们在数据结构中的插入操作未能正确执行。推测问题发生在某个阶段字符串没有被加入,导致热重载后的状态不完整,这是一个明显的 bug。
相关文章:
游戏引擎学习第254天:重新启用性能分析
运行游戏并尝试让性能分析系统恢复部分功能 我们现在的调试系统这几天基本整理得差不多了,因此我们打算开始清理一些功能,比如目前虽然已经在收集性能分析数据,但我们没有办法查看或有效利用这些信息。今天的计划可能会围绕这方面展开&#…...
性能测试工具篇
文章目录 目录1. JMeter介绍1.1 安装JMeter1.2 打开JMeter1.3 JMeter基础配置1.4 JMeter基本使用流程1.5 JMeter元件作用域和执行顺序 2. 重点组件2.1 线程组2.2 HTTP取样器2.3 查看结果树2.4 HTTP请求默认值2.5 JSON提取器2.6 用户定义的变量2.7 JSON断言2.8 同步定时器&#…...
【Hive入门】Hive性能调优之Join优化:深入解析MapJoin与Sort-Merge Join策略
目录 前言 1 Hive Join操作基础 1.1 Join操作的类型与挑战 1.2 Hive Join执行机制 2 MapJoin优化策略 2.1 MapJoin原理 2.2 MapJoin适用场景 2.3 MapJoin关键参数 3 Sort-Merge Join优化策略 3.1 Sort-Merge Join原理 3.2 Sort-Merge Join优势 3.3 关键配置参数 3…...
【Unity】使用XLua实现C#访问Lua文件
先引入XLua文件中的Plugins和XLua文件夹于Unity项目的Asset文件中 XLua_github链接 建立Lua虚拟机:LuaEnv luaEnv new LuaEnv(); 关闭虚拟机,及时释放资源:luaEnv.Dispose(); Resources文件夹下加载lua文件(假设文件路径为Resour…...
AXI中的out of order和interleaving的定义和两者的差别?
AXI中的out of order和interleaving的定义和两者的差别 摘要:在 AXI (Advanced eXtensible Interface) 协议中,Out-of-Order 和 Interleaving 是两个与事务处理顺序和数据传输相关的概念,它们都与 AXI 协议支持的多事务并发处理能力有关&…...
生产级RAG系统一些经验总结
本文将探讨如何使用最新技术构建生产级检索增强生成(RAG)系统,包括健壮的架构、向量数据库(Faiss、Pinecone、Weaviate)、框架(LangChain、LlamaIndex)、混合搜索、重排序器、流式数据接入、评估策略以及实际部署技巧。 引言:检索增强生成的力量 大型语…...
sftp连接报错Received message too long 168449893
sftp连接报错Received message too long 168449893 一、openEuler传文件报错二、分析问题三、解决问题endl 一、openEuler传文件报错 [rootRocky9-12 ~]# scp apache-tomcat-10.1.33.tar.gz root10.0.0.14:Authorized users only. All activities may be monitored and report…...
Java中修饰类的关键字
Java中修饰类的关键字 在web编程课上,老师提问了Java中各种修饰类的关键字的用途和区别,一时间我头脑空白,现在课后重新梳理一遍Java中修饰类的各种关键字的区别和用法。在Java编程中,修饰类的关键字起着至关重要的作用ÿ…...
2025年人工智能火爆技术总结
2025年人工智能火爆技术总结: 生成式人工智能 生成式人工智能可生成高质量的图像、视频、音频和文本等多种内容。如昆仑万维的SkyReels-V2能生成无限时长电影,其基于扩散强迫框架,结合多模态大语言模型和强化学习等技术,在运动动…...
脑机接口技术:开启人类与机器的全新交互时代
在科技飞速发展的今天,人类与机器的交互方式正经历着前所未有的变革。从最初的键盘鼠标,到触摸屏,再到语音控制,每一次交互方式的升级都极大地提升了用户体验和效率。如今,脑机接口(Brain-Computer Interfa…...
Arduino程序函数详解与实际案例
一、Arduino程序的核心架构与函数解析 Arduino程序的核心由两个函数构成:setup() 和 loop()。这两个函数是所有Arduino代码的骨架,它们的合理使用决定了程序的结构和功能。 1.1 setup() 函数:初始化阶段 setup() 函数在程序启动时仅执行一次,用于完成初始化配置,例如设置…...
2025年RAG技术发展现状分析
2025年,大模型RAG(检索增强生成)技术经历了快速迭代与深度应用,逐渐从技术探索走向行业落地,同时也面临安全性和实用性的新挑战。以下是其发展现状的综合分析: 一、技术架构的持续演进 从单一到模块化架构 …...
C++11新特性_范围-based for 循环
based for 循环介绍 范围 - based for 循环(Range-based for loop)是 C11 引入的一种新的 for 循环语法,它可以更简洁地遍历容器和数组。 遍历数组:定义了一个整数数组 arr,使用范围 - based for 循环 for (int num :…...
小牛电动:荣登央视舞台,引领智能出行新潮流
在这个科技飞速发展的时代,出行方式也在不断地变革与创新。而在两轮电动车领域,有一个品牌凭借其卓越的技术、独特的设计和优质的服务脱颖而出,那就是小牛电动。近日,小牛电动荣登央视舞台,成为备受瞩目的焦点…...
Three.js在vue中的使用(一)-基础
Three.js 是一个基于 WebGL 的 JavaScript 3D 图形库,它简化了在网页中创建和渲染 3D 场景的复杂性。Three.js 提供了丰富的功能,如光照、材质、几何体、动画、控制器等,使得开发者可以快速构建交互式的 3D 应用。 🧠 Three.js 原理概述 1. WebGL 基础 Three.js 底层使用…...
开发板型号 ESP32-DevKitC-32模块型号 ESP32-WROOM-32 和主控芯片 ESP32-D0WDQ6-V3
以下是关于开发板型号 ESP32-DevKitC-32、模块型号 ESP32-WROOM-32 和主控芯片 ESP32-D0WDQ6-V3 的详细介绍: 开发板型号:ESP32-DevKitC-32 概述:ESP32-DevKitC 是乐鑫推出的一款基于 ESP32 模组的小型开发板,板上模组的绝大部…...
【C语言练习】015. 声明和初始化指针
015. 声明和初始化指针 015. 声明和初始化指针1. 声明指针示例1:声明一个指向整数的指针2. 初始化指针示例2:将指针初始化为`NULL`示例3:将指针初始化为某个变量的地址示例4:将指针初始化为动态分配的内存地址3. 使用指针访问和修改变量的值示例5:使用指针访问和修改变量的…...
手撕哈希表
引入:unordered_set /map是什么? 库里面除开set和map,还有unordered_set 和 unordered_map,区别在于: ①:set和map的底层结构是红黑树,而unordered_set和unordered_map的底层是哈希表 ②&…...
编程题python常用技巧-持续
1.字典 1.1排序 在Python中,要按照字典的值进行排序,可以按照以下步骤操作: 方法说明 获取键值对列表:使用 dict.items() 获取字典的键值对视图。排序键值对:使用 sorted() 函数,并通过 key 参…...
大模型蒸馏技术
提问:请写一篇关于蒸馏大模型的详细解说(论文),要求配有图并不少于8000字。 Deepseek: 大模型蒸馏技术:原理、方法与产业实践 ——基于知识压缩与效率优化的深度解析 目录 引言:大模型时代的…...
深入理解C语言中的整形提升与算术转换
深入理解C语言中的整形提升与算术转换 一.整形提升:概念与原理 在C语言中,整形提升(Integer Promotion)是一个重要但容易被忽视的概念。它指的是在表达式中,任何小于int类型的整型(如char、short…...
企业经营系统分类及功能详解
近年来互联网行业下行,招聘少,要求离谱,年龄学历背景已经卡的死死的,技术再突出也没用。 但对于软件开发来说,互联网只是一小部分,企业级系统软件开发,虽然不如互联网大起大落,但重…...
IRF2.0IRF3.1
1、IRF3定义 IRF3是一种能够提高网络接入层的接入能力和管理效率的纵向网络整合虚拟化技术,采用IEEE 802.1BR标准协议实现。IRF3将多台PEX设备(Bridge Port Extender)连接到父设备(Parent device)上,将每台…...
【C++】类和对象【中下】
目录 一、类与对象1、运算符重载1.2 赋值运算符重载1.3 <<运算符和>>运算符1.4 前置与后置 2、 const成员函数3、取地址运算符重载 个人主页<—请点击 C专栏<—请点击 一、类与对象 本期的主题是一步步完善日期类的编写,将要讲解的知识融入在代…...
ThreadLocal详解
什么是 ThreadLocal? ThreadLocal 是 Java 中的一个工具类,用于为每个线程提供独立的变量副本,使得每个线程可以独立操作自己的变量,避免多线程环境下的数据竞争问题。它的核心思想是线程封闭(Thread Confi…...
Vue3 + OpenLayers 企业级应用进阶
1. 企业级架构设计 1.1 微前端架构集成 // src/micro-frontend/map-container.ts import { Map } from ol; import { registerMicroApps, start } from qiankun;export class MapMicroFrontend {private map: Map;private apps: any[];constructor(map: Map) {this.map map;…...
如何提升自我执行力?
提升个人执行力是一个系统性工程,需要从目标管理、习惯养成、心理调节等多方面入手。 以下是具体方法,结合心理学和行为科学原理,帮助你有效提升执行力: 一、明确目标:解决「方向模糊」问题 1. 用SMART原则设定目标 …...
L3-041 影响力
下面给出基于“切比雪夫距离”(Chebyshev 距离)之和的高效 O(nm) 解法。核心思想是把 ∑ u 1 n ∑ v 1 m max ( ∣ u − i ∣ , ∣ v − j ∣ ) \sum_{u1}^n\sum_{v1}^m\max\bigl(|u-i|,|v-j|\bigr) u1∑nv1∑mmax(∣u−i∣,∣v−j∣) 拆成两个…...
【ESP32】st7735s + LVGL使用-------图片显示
【ESP32】st7735s + LVGL使用-------图片显示 1、文件准备2、工程搭建3、代码编写4、应用部分5、函数调用6、显示效果移植部分参考这个博客: 【ESP32】st7735s + LVGL移植 1、文件准备 本次图片放在内部存储,先使用转换工具将要显示的图片转换好。 文件名保存为xx.c,xx这…...
MERGE存储引擎(介绍,操作),FEDERATED存储引擎(介绍,操作),不同存储引擎的特性图
目录 MERGE存储引擎(合并) 介绍 创建表 语法 示例 查看.mrg文件 操作 查询结果 示例 重建逻辑表 FEDERATED存储引擎 结盟 介绍 编辑 应用场景 操作 开启 创建表 对本地表进行数据插入 EXAMPLE存储引擎 不同存储引擎的特性编辑 MERGE存储引擎(合并) 介绍…...
初学者如何学习AI问答应用开发范式
本文是根据本人2年大模型应用开发5年小模型开发经验,对AI问答应用的开发过程进行总结。 技术范式 现在超过80%的AI问答是 提示词 大模型, 然后就是RAG 方案,这两种无疑是主流方案。 1、提示词大模型 适合于本身业务不超过大模型的知识范围…...
GESP2024年6月认证C++八级( 第二部分判断题(1-5))
判断题2: #include <iostream> #include <iomanip> using namespace std;int main() {double a 1e308;double b 1e-10;double orig_a a, orig_b b;a a b;b a - b;a a - b;cout << fixed << setprecision(20);cout << "…...
npm命令介绍(Node Package Manager)(Node包管理器)
文章目录 npm命令全解析简介基础命令安装npm(npm -v检插版本)初始化项目(npm init)安装依赖包(npm install xxx、npm i xxx)卸载依赖包(npm uninstall xxx 或 npm uni xxx、npm remove xxx&…...
小刚说C语言刷题—1602总分和平均分
1.题目描述 期末考试成绩出来了,小明同学语文、数学、英语分别考了 x、y、z 分,请编程帮助小明计算一下,他的总分和平均分分别考了多少分? 输入 三个整数 x、y、z 分别代表小明三科考试的成绩。 输出 第 11行有一个整数&…...
python类私有变量
在Python中,要将一个属性定义为类的内部属性(也就是私有属性),通常会在属性名称前加一个下划线(_)或两个下划线(__)。这两种方式有不同的效果: 单下划线(_&a…...
前端如何转后端
前端转后端是完全可行的,特别是你已经掌握了 JavaScript / TypeScript,有一定工程化经验,这对你学习如 Node.js / NestJS 等后端技术非常有利。下面是一条 系统化、实践导向 的路线,帮助你高效完成从前端到后端的转型。 ✅ 一、评…...
数字智慧方案5976丨智慧农业顶层设计建设与运营方案(59页PPT)(文末有下载方式)
详细资料请看本解读文章的最后内容。 资料解读:智慧农业顶层设计建设与运营方案 在现代农业发展进程中,智慧农业成为推动农业转型升级、提升竞争力的关键力量。这份《智慧农业顶层设计建设与运营方案》全面且深入地探讨了智慧农业的建设现状、需求分析、…...
软件工程国考
软件工程-同等学力计算机综合真题及答案 (2004-2014、2017-2024) 2004 年软工 第三部分 软件工程 (共 30 分) 一、单项选择题(每小题 1 分,共 5 分) 软件可用性是指( )…...
linux python3安装
1 安装依赖环境 yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel 2 mkdir -p /usr/python3 3 cd usr/python3; tar -zxvf Python-3.8.3.tgz;cd Python-3.8.3 4 ./confi…...
软件测评中心如何保障软件质量与性能?评测范围和标准有哪些?
软件测评中心对保障软件质量与性能有关键作用,它像软件世界里的质量卫士,会评测各类软件,能为用户选出真正优质好用的软件,我将从多个方面向大家介绍软件测评中心。 评测范围 软件测评中心的评测范围很广,它涵盖了常…...
从MCP基础到FastMCP实战应用
MCP(https://github.com/modelcontextprotocol) MCP(模型上下文协议) 是一种专为 基于LLM的工具调用外部工具而设计的协议 , 本质上是 LLM ↔ 工具之间的RPC(远程过程调用) 的一种安全且一致的处理方式, 是…...
【云备份】服务端工具类实现
1.文件实用工具类设计 不管是客户端还是服务端,文件的传输备份都涉及到文件的读写,包括数据管理信息的持久化也是如此,因此首先设 计封装文件操作类,这个类封装完毕之后,则在任意模块中对文件进行操作时都将变的简单化…...
如何在Cursor中使用MCP服务
前言 随着AI编程助手的普及,越来越多开发者选择在Cursor等智能IDE中进行高效开发。Cursor不仅支持代码补全、智能搜索,还能通过MCP(Multi-Cloud Platform)服务,轻松调用如高德地图API、数据库等多种外部服务ÿ…...
PB的框架advgui反编译后控件无法绘制的处理(即导入pbx的操作步骤)
advguiobjects.pbl反编译后,涉及到里面一个用pbni开发的一个绘制对象需要重新导入才可以。否则是黑色的无法绘制控件: 对象的位置在: 操作: 导入pbx文件中的对象。 恢复正常: 文章来源:PB的框架advgui反编译…...
第 11 届蓝桥杯 C++ 青少组中 / 高级组省赛 2020 年真题,选择题详细解释
一、选择题 第 2 题 在二维数组按行优先存储的情况下,元素 a[i][j] 前的元素个数计算如下: 1. **前面的完整行**:共有 i 行,每行 n 个元素,总计 i * n 个元素。 2. **当前行的前面元素**:在行内&#x…...
Python 装饰器基础知识科普
装饰器定义与基本原理 装饰器本质上是一个可调用的对象,它接收另一个函数(即被装饰的函数)作为参数。装饰器可以对被装饰的函数进行处理,之后返回该函数,也可以将其替换为另一个函数或可调用对象。 代码示例理解 有…...
数字基带信号和频带信号的区别解析
数字基带信号和数字频带信号是通信系统中两种不同的信号形式,它们的核心区别在于是否经过调制以及适用的传输场景。以下是两者的主要区别和分析: 1. 定义与核心区别 数字基带信号(Digital Baseband Signal) 未经调制的原始数字信号…...
Nginx Proxy Manager 中文版安装部署
目录 Nginx Proxy Manager 中文版安装部署教程一、项目简介1.1 主要功能特点1.2 项目地址1.3 系统架构与工作原理1.4 适用场景 二、系统要求2.1 硬件要求2.2 软件要求 三、Docker环境部署3.1 CentOS系统安装Docker3.2 Ubuntu系统安装Docker3.3 安装Docker Compose 四、安装Ngin…...
类和对象(拷贝构造和运算符重载)下
类和对象(拷贝构造和运算符重载)下 这一集的主要目标先是接着上一集讲完日期类。然后再讲一些别的运算符的重载,和一些语法点。 这里我把这一集要用的代码先放出来:(大家拷一份代码放在编译器上先) Date.h #include <iostream> #include <cassert> …...
Codeforces Round 1008 (Div. 2) C
C 构造 题意:a的数据范围大,b的数据范围小,要求所有的a不同,考虑让丢失的那个a最大即可。问题变成:构造一个最大的a[i] 思路:令a2是最大的,将a1,a3,a5....a2*n1,置为最大的b,将a4,a…...