游戏引擎学习第187天
看起来观众解决了上次的bug
昨天遇到了一个相对困难的bug,可以说它相当棘手。刚开始的时候,没有立刻想到什么合适的解决办法,所以今天得从头开始,逐步验证之前的假设,收集足够的信息,逐一排查可能的原因,最终找到罪魁祸首。
然而,与其说是自己独自解决问题,不如说是通过观众帮助找到了bug。虽然我自己并不知道他们找到的具体问题是什么,但从论坛上看到有人发布了关于bug可能原因的猜测,他们的分析让我觉得很有道理。看完后,我认为他们的猜测很可能接近问题的核心。
这种情况就像是,如果是自己一个人在办公室里工作,会把问题拖延几个小时,可能还没找到解决办法。而现在就像是在办公室里有一群程序员可以一起讨论,大家都在看问题,并且有人很快就找到了线索。
总结一下,昨天的问题让人觉得很棘手,但今天我开始从观众的反馈中获得了新的线索,可能这就是意外的好处所在。
描述这个bug的影响
现在我们看到的是一个看起来相当有效的性能分析,刚开始时,我们确实看到了合理的性能数据增长,但是不幸的是,在初始的一波之后,我们就没有看到任何数据了。这个现象很有趣,因为现在我意识到,仍然什么都看不见。
最初的一些数据大概可以看到,可能是因为第一次运行时,缓存和加载的过程比较慢,导致延迟,这些延迟反而帮助了我们。因此,昨天我们追踪到的bug其实我们已经知道是什么问题了,只是不清楚它的原因。
具体的bug是这样的:每次我们通过线程处理时,我们都会在打开一个计时块时进行记录,并且会寻找对应的关闭块。当我们打开块时,栈会增加,而关闭块时栈会减少。然而,我们发现这个过程本应该没有问题,但在某些时刻,我们收到了大量的调试记录,里面有一些打开的块没有对应的关闭块,这让我们感到很困惑。
这是一种常见的情况,就是在追踪调试记录时,出现了意外的行为,特别是在栈的管理上。
即使有很多经验,难以解决的bug仍然会发生
就像之前说的,我编程已经有三十年了。在调试问题时,有个普遍的规律就是,由于我编程时间久了,通常我能在十到二十分钟内调试好一个问题,即使是比较复杂的bug,因为我知道问题可能出在哪里,我去找找看,最终就会解决,这种情况在也经常发生。
但是偶尔,即便是编程这么多年,至少像我这样的人,还是会遇到让人完全摸不着头脑的bug。这种bug确实会把人难住,真的不知道问题出在哪。有时候解决这样的bug可能需要几个小时,甚至几天。有时这些bug就是真的非常难以捉摸。
有过这样的经历,编译器输出错误代码,追踪这种bug真的非常费时间;也有过操作系统本身的bug,导致问题出现。遇到这种情况,根本问题本身有缺陷,找起问题来确实很花时间,可能需要几天才能解决,确实是那种非常棘手的难题。
这个bug并不复杂,但它的来源超出了我们认为可能的原因范围
有时候,bug并不复杂,甚至是一个非常简单的bug。那么问题来了,为什么这个简单的bug找起来这么费劲,而其他一些看似简单的bug却能很快解决?
原因就在于直觉。由于多年的编程经验,会对代码的某些部分形成一定的假设。在调试时,通常会围绕这些假设去寻找问题所在。大多数时候,bug确实就出现在这些预设的范围内,因此调试过程通常是通过验证一些假设来缩小可能出错的范围。
但问题在于,通常在调试的过程中,我们并没有检查程序中的所有部分。因为有些部分可能也出错,但显然不可能去检查所有的地方。你会在某些地方做出假设,认为它们是对的,其他地方则认为不需要验证。问题就出在当你没有正确划定这个界限时,导致遗漏了某些地方,而这些地方恰恰可能就是bug所在。
这就像在现实生活中找东西一样。如果你丢了钥匙,找了很久也没有找到,最后发现它在一个非常合理的地方。问题是,你当时并没有想到它会在那里,你根本没检查到那个地方。因此,当你找钥匙很顺利时,通常是因为你在一开始就猜对了几个可能的地方,最终找到了它。
而如果钥匙在一个特别难找的地方,可能就是你当时就经过了那个地方,甚至就站在钥匙旁边,但你完全没意识到。直到很久之后才会想起来原来钥匙就在那个地方,而这时候已经过去了很长时间,可能还会错过几天才能找到。
这次遇到的bug就是这种情况。
这个bug可能与不正确的翻译单元索引有关
在论坛上,大家提出了一个可能的原因,认为我们遇到的问题是因为翻译单元索引出了问题。我们使用了一种新的方法来追踪每个调试位置的唯一标识符,这个方法之前没尝试过,但它正是因为我们只有很少的翻译单元才能够实现。通常情况下,由于翻译单元数量很多,这种方法是不可行的,但我们只有三个翻译单元,因此可以尝试这种方法。
为了实现这种方法,我们需要一个翻译单元的编号来确认每个调试记录对应的位置。调试记录的索引帮助我们唯一地标识出是哪一个翻译单元的记录。如果翻译单元索引不正确,就无法知道该调试记录到底属于哪个翻译单元,是否属于主代码、优化代码还是平台层代码。
这个方法是为了尝试用计数器系统来追踪调试位置而做出的妥协。然而,事实证明,最初我们是做对的,我原本将RecordDebugEvent
(或者类似的代码)做成了宏,这是最终希望实现的方式。
如果编译器没有内联RecordDebugEvent,可能会生成错误的翻译单元索引
在某个阶段,我们将 RecordDebugEvent
改为了内联函数,以便更容易调试和观察。但是,这样做引发了一个问题。如果这个函数是内联的,但编译器没有在每个地方都内联它,即使我们尝试控制,也不能保证每次都内联。编译器完全有可能不内联这个函数。如果编译器不内联,那么它就会在每个翻译单元中生成这个函数的一个版本。
我们的程序有三个翻译单元,每个翻译单元都调用了 RecordDebugEvent
,这就意味着每个翻译单元中都会有一个 RecordDebugEvent
的版本。然而,在链接时,链接器只会使用其中一个版本。这是链接器的工作方式,通常链接器不会试图保持这些版本分开,至少根据我所了解,链接器不会这么做。虽然可能在某些情况下,链接器应该确保每个翻译单元都有一个独立的版本,但这方面的规则我并不十分清楚,也可能我理解的有误。
在论坛上,大家提出了一个假设,经过检查后似乎是正确的。假设是,编译器在每个翻译单元中生成了不同的 RecordDebugEvent
函数,并将正确的翻译单元信息放入其中。然而,链接时,链接器实际上丢弃了其中一个版本,这意味着无论哪个翻译单元调用 RecordDebugEvent
,它最终使用的都是同一个翻译单元的版本。
这种情况导致了调试记录的索引发生冲突,从而使得调试记录的开闭操作看起来像是错误的,实际上并没有按预期的方式发生。
检查这个理论
这是他们的假设,听起来确实是一个非常合理的猜测。实际上,检查这个问题有一个相对简单的方法。我们可以检查一下翻译单元的索引,看看它是否有可能不等于零或者二。如果程序中的优化翻译单元和主翻译单元是一起编译的,那么在调试事件中应该会看到翻译单元的索引为零和一。
我打算查看是否存在错误的函数调用,具体是看我们在调试时是否能看到这两个翻译单元的索引。如果我们能看到零和一,那么就意味着问题可能不出在这里,尽管还可能有其他原因导致问题。如果我们从未看到过翻译单元的索引为一,那么这就证明我们只得到了一个 RecordDebugEvent
的版本,导致了调试记录的错误。
因此,我会查看在运行过程中,当我们遍历这些事件时,是否能断言事件的翻译单元索引不等于零。这个断言应该会立即触发,因为我们预计调试事件中会有翻译单元零和一。果然,事件中的翻译单元零和一都存在。
接下来,我还将检查翻译单元二的情况,假设这意味着所有三个翻译单元都在运行。
目前来看,问题的根源还不完全明确,因为虽然翻译单元一(优化过的翻译单元)存在,但我们仍然不确定从优化单元出发的所有路径是否都能得到正确的翻译单元。因此,我希望进一步检查优化过的部分。
game_optimized中的某个时间函数是否工作不正常?
我想看看时间函数到底是怎么工作的。实际上有两个TIMED_FUNCTION。所以可能其中一个或者两个函数并没有正确地记录信息。我打算进入这两个函数,逐步调试,看看它们的行为。同时,为了让调试过程更方便,我还会采取一些额外的措施来帮助我们更好地理解问题所在。
禁用优化
打算查看在完全关闭优化模式下,程序的运行情况。换句话说,如果我们关闭所有路径的优化,不进行优化编译,那么会发生什么呢?通过这个测试,发现很有意思的现象,似乎能支持之前的假设。因为如果调试信息在没有优化的情况下能正常显示,那么可能确实是在优化模式下出现了问题。不过这只是其中一个线索,仍然需要进一步找出问题的根本原因。虽然目前这样做有些不太理想,但接下来会考虑一个更干净的方式来存储数据。
在调试模式下,bug没有复现
在调试模式下,似乎没有正常显示回调信息,这开始暗示可能是因为翻译单元的问题,也就是说在优化过程中,可能有一个翻译单元被优化进了代码中,而另一个则没有。这可能导致了一些调试信息只有部分显示,虽然我们看到有一些“1”和“0”的输出,但目前仍不完全确定到底是怎么回事,可能是由于优化速度的问题导致的。现在还不清楚最终原因,只是在尝试调查这一现象,看是否有道理。从目前的情况来看,似乎必须将优化设置为更高的级别,或者至少高于低优化模式,才能正确地捕获到相关信息。虽然问题显然存在,无法否认,但目前还没有完全确认它的根本原因。
单步调试优化后的代码
为了深入排查问题,首先设置了一个断点,在 RecordDebugEvent
函数中,并进行了调试运行。接着,冻结了所有其他线程,只专注于当前的线程,以便能更好地观察调试过程。在调试时,检查了全局调试表格,尝试查看正在记录的信息。观察到当前记录的调试数据接近之前的一些数据,从而确认了当前代码的执行状态。此外,还尝试查看 RecordDebugEvent
的行为,检查是否能够获取更多关于执行过程的线索。
RecordDebugEvent没有被内联
在调试过程中,观察到的现象与预期的情况基本一致,表明可能确实是之前提到的那个bug。当前执行的函数并没有被内联,这很可能导致相关的调试记录没有被正确地更新,甚至可能是因为这些记录被优化掉了。
接下来,尝试查看翻译单元(Translation Unit)的具体内容,看看是否能观察到调试记录的写入情况,但目前并没有看到相关更新。可能存在偏差,无法准确确定需要查看哪些数据,或者所期望的调试记录索引并没有正确写入。
在此基础上,考虑查看汇编代码,分析实际执行的指令,以便确认程序运行时到底执行了什么。
是的,翻译单元错了,论坛说得对
通过分析,发现程序在执行过程中确实选择了错误的翻译单元,导致了问题的发生。调试记录的翻译单元被错误地抛弃,这正是问题所在。这一发现与论坛上某些用户的分析完全一致,说明他们的推测是正确的。正是因为选择了错误的翻译单元,导致调试记录被处理错误,最终导致了预期外的行为。
将RecordDebugEvent改为宏修复了问题,但…
解决这个问题的方法其实非常简单,就是将相关的函数转换成宏。这样,宏会在每次调用时展开,这样就可以确保它在正确的地方被处理。通过强制内联,问题就能够得到解决。经过修改后,问题确实得到了修复。这个问题的根源在于翻译单元的处理,转换成宏后,问题就不再出现了。这是一个非常直接的修复方案。
…我们应该去除与翻译单元索引相关的代码,反正它增加了复杂性,容易引入微妙的bug
在解决了这个问题之后,有一个重要的思考,就是使用每个翻译单元的方式可能已经到了该结束的时候。虽然这次尝试是一次实验,虽然这个方法可能有一些有用的方面,但我觉得这个问题很微妙,主要是因为我们在使用计数器时引入了额外的复杂性。而我现在感觉不太舒服的是,依赖这样的方式会增加不必要的复杂性,这样的 bug 很难察觉。
虽然引入一些不完美的东西(比如 Jenkins)并不会直接导致灾难,但这次的 bug 就是一个很好的例子,提示我们这种方式并不可靠。我觉得这是时候放弃这种方式了,毕竟尝试新方法是对的,但通过这次的经验,我感觉这并没有带来足够的好处,反而增加了复杂性,而这种复杂性带来了代价。对于我们的目标来说,最终这个复杂度似乎并不值得。
我们暂时不会删除翻译单元索引
目前虽然问题得到了修复,但我认为还是应该考虑放弃翻译单元索引的方式,转而采用更标准的单次哈希表。这种做法的复杂性已经带来了经典的问题,虽然现在修复了这个 bug,但我对于这种方式还是有所担忧,觉得它可能并不是最佳的方案。
尽管现在能够正常工作,但如果以后再出现类似的 bug,我会建议直接替换这种实现,而不是继续调试。毕竟,目前对这种方式的信任度不高,也没有足够的信心保证它在更复杂的情境下能稳定运行。换句话说,如果未来出现问题,我宁愿换掉它而不是再继续调试。
总的来说,这个 bug 解决了,现在的情况应该已经比较稳定。但接下来还有一堆工作需要处理,我们需要专注于生产一些有用的可视化结果,而不是再在复杂的实现上浪费太多时间。
回到可视化。让我们避免生成在调试图表中看不见的区域
首先,我们不希望在区域无法真正显示时还生成这些区域。为了避免这种情况,我们可以在添加区域时,先检查一下所记录的时间差是否足够大,能够实际反映出一个可见的区域。为了实现这一点,可以为每个区域设置最小和最大时间(min 和 max),并判断这些时间差是否足够大,以决定是否记录该区域。
具体来说,如果最大时间和最小时间的差值(max - min)小于某个预设的阈值,那么就可以忽略这个区域。这个阈值(例如,我们可以假设时间条被分为100份,小于其中1%的区域就不值得记录)将帮助过滤掉那些微小的、不可见的区域。这样就可以避免生成那些无用的、看不见的、非常短的时间片段。
通过这种方式,可以让记录的区域更加有意义,避免了那些只有极短时间跨度的区域,避免了生成大量微小且不易察觉的区域。
引入选项来编译掉分析代码
为了提高运行效率,首先需要确保调试信息能够被编译掉。调试信息会导致运行变慢,因此需要能够在不同的场景中开启和关闭调试功能,确保在调试时能控制它的开关。
目前,解决方案是在代码中使用条件编译来控制调试信息的插入。具体来说,可以通过设置编译器的标志来启用或禁用调试信息。例如,在启用游戏分析(profile)模式时,才会插入调试相关的代码。如果没有启用分析,调试相关的代码将被完全剔除,不会进入编译的最终代码中,这样就避免了不必要的性能损耗。
通过这种方式,代码在没有启用分析时,会保持快速运行,调试信息完全不存在,不会影响到性能和图表绘制。这种方法保证了调试信息只有在需要时才会出现,优化了性能,同时也保留了必要的调试功能。
总之,调试信息的插入是可以根据需求进行灵活控制的,可以根据不同的构建设置决定是否包括这些调试信息,以此来提高性能,确保调试与正常运行的平衡。
根据调试显示,我们应该运行在更高的帧率上
目前,程序的运行速度明显变慢,帧率看起来远低于预期。即使从分析的数据显示,程序的运行时间和帧率看起来是正常的,然而实际的体验却感觉非常缓慢。这让人怀疑可能是某些地方的绘制方式出了问题,或者是某些地方的性能被低估了。
具体来说,虽然从图表上看,程序的运行时间与理论上的帧率(比如每秒30帧)差距不大,但实际帧率却远低于预期,甚至像是每秒只有两到五帧,而不是应该达到的30帧。这种差距让人困惑,可能存在一些我们没注意到的问题。
为了解决这个问题,可以通过添加一个帧率计数器,使用类似“查询性能计数器”的方法来进一步诊断和分析帧率的实际情况。通过这样的方式,可以准确捕捉帧率,并检查是否真的存在性能瓶颈,帮助找出程序中可能的性能问题。
验证rdtsc测量与墙钟时间的对比
目前,可以考虑通过改进时间的显示和测量方式来更好地调试性能问题。首先,可以利用已有的“翻转时钟”(flip wall clock)和计时器(encounter)来优化调试系统。通过将这些时钟信息传递给调试系统,可以更精确地验证时间戳计数(TSC)测量结果与墙上时钟时间之间的关系,确保它们至少在某种程度上具有一致性。这样做可以让调试过程更加清晰和准确。
另外,可以通过记录整个代码块的 RTT(Round Trip Time)值来进一步验证时钟时间的精度。例如,可以在帧标记处插入墙上时钟的时间,这样就能在执行过程中实时记录下每一帧的时间和对应的墙上时钟时间。通过对比已知的墙上时钟时间和程序内的周期计时,可以确认它们之间是否存在合理的关联。
为了实现这一点,可以考虑将帧标记放到重置点(renault)处,这样在最初的地方就可以插入墙上时钟时间,从而捕捉到开始时的时间数据。虽然实现起来可能具有一定的挑战,但这一过程对于精确跟踪每帧的执行情况非常有帮助,有助于在调试时发现潜在的性能瓶颈。
在FRAME_MARKER调用时记录墙钟时间
为了进一步优化调试记录并提高精确度,可以考虑在调试系统中增加墙上时钟(wall clock)时间的记录。在目前的设计中,可以通过整合线程ID、核心索引和帧标记等信息,将它们存储在一个结构体中,这样可以在记录调试信息时更准确地跟踪每个操作的具体时刻。
对于墙上时钟时间的获取,可以通过调用系统的时钟函数,然后将返回的值转换为秒数,以便与程序内的周期计时进行对比。为了实现这一点,首先需要获取64位的时钟计数器,然后通过将其除以系统的时钟频率来得到一个秒数值。这样就能在每个帧标记处插入墙上时钟时间,并确保能够与程序中的计时信息进行对比。
考虑到精度问题,如果选择按照这种方法实现,可能会损失一定的精度,因为直接将时钟频率除以返回的计数器值可能不够精确。为此,也可以考虑在时钟时间的计算中引入更高精度的算法,或者使用已有的计时器来记录程序执行的具体时间。
此外,在帧标记的设计上,考虑到程序的启动和第一帧之间的时间差,可能需要做额外的标记和调整。虽然这会增加一些复杂度,但它能为进一步分析提供更完整的时间序列信息。总的来说,这些改进旨在提升调试的可操作性,使调试记录能够准确反映程序运行时的详细时间情况,从而更好地定位性能瓶颈和潜在问题。
我们的显示与帧率不一致,因为没有考虑到整理调试记录所需的时间
问题的根本原因在于调试记录的收集时间没有被纳入性能分析中,这可能是导致性能配置文件(profile)与实际表现不匹配的一个关键因素。调试记录的收集时间通常会占用相当多的资源,因此它的计算需要被考虑进来。
为了解决这个问题,可以通过记录和计算调试事件的时间开销来改善性能分析。具体来说,可以在帧标记的过程中加入一个新的计时器,用于计算每次帧处理的时间差。首先,获取上一帧的计时器值(即last counter
),然后与当前计时器值进行对比,计算出实际的秒数差。这个差值即为当前帧的时间(seconds elapsed
)。
通过将这些计算集成到调试记录中,每次记录调试事件时,除了标准的调试信息外,还需要添加帧时间。这样就可以确保性能分析更加准确,能够反映出包括调试事件收集在内的所有时间消耗,从而帮助更好地理解程序的运行效率和性能瓶颈。
有选择地设置SecondsElapsed而不是ThreadId和核心索引
在调试事件中,需要根据特定条件适当设置事件参数。为了实现这一点,可以定义一个宏,该宏在记录调试事件时自动处理常见的操作,例如设置事件索引和类型。这样,框架标记的调试事件就能在记录时,按照不同的条件来设置秒数。
首先,应该定义一个调试事件的宏,它包含常见的调试操作。在框架标记中,可以使用这个宏来记录调试事件,但同时为每个事件设置不同的参数。这样做的目的是使得记录的每个事件都包含正确的时间戳信息,以便更准确地分析性能。
在实际实现过程中,可能需要调整事件的记录方式。尤其是当框架标记发生在结束时,而不是开始时,处理方式需要有所不同。例如,记录开始时的时钟值,而不是结束时的时钟值。尽管这增加了一些复杂性,但不影响框架的正常工作,只是意味着在遇到框架标记时,所有关于该帧的信息已经被收集完毕。
每次框架标记都会记录当前的帧数据,并在此基础上创建一个新的帧,用于记录随后的事件。这些事件会有新的开始时钟值,而不是前一帧的结束时钟。由于当前帧的信息已经被完全收集,所以下一帧的时间会基于新的时钟值进行记录。
这种做法的一个挑战是,无法在当前帧中即时获得秒数差(wall clock seconds elapsed),因此需要在后续的框架中进行处理。尽管这个过程略显繁琐,但仍然能够准确地记录每个调试事件的时间戳,从而帮助性能分析。
总体而言,这种方法有助于提高调试数据的准确性,并使得性能分析更加精确,能够反映调试事件对程序运行时性能的实际影响。
测试今天的新增内容。奇怪的是,FrameWait和FrameDisplay的时间增加了
现在可以看到这一条线变长了,比之前要长。然而,奇怪的是,并没有真正修改这部分的信息。虽然移动了框架标记的位置,但这部分数据并没有被计算进去。因此,这种变化显得有些异常,说明可能在某个地方出现了错误。
目前运行的代码与之前相同,因此理论上不应该导致不同的形状。然而,现在的情况是,这条曲线出现了较长的尾部,特别是在**帧等待(frame wait)和帧显示(frame display)**这两个部分。这两个部分的时间变长了,但原因尚不明确。这让人困惑,因为它们不应该变大,毕竟代码逻辑没有进行相应的修改。
从结果来看,这可能是某些细微的错误导致的。例如,某些未被注意到的修改影响了时间测量的方式,或者是数据收集的过程出现了意外的偏差。此外,虽然目前还没有将新的时间计算方式纳入统计,但似乎已经对最终的曲线产生了影响。
为了进一步验证,会尝试真正计算这部分数据,并看看最终结果是否与预期一致。这将有助于确定当前代码的执行情况,并找出导致曲线变化的具体原因。
添加DebugCollation计数器
现在来看一下,目前的**周期计数(cycle count)已经不再需要单独计算了。因为现在的代码已经在多个地方获取了周期计数,所以实际上没有必要再在平台层(platform layer)**执行这项工作。
之前在平台层进行周期计数的逻辑,现在已经变得多余。因此,这部分代码可以被移除,而不会影响整体功能。这样可以减少不必要的计算,使代码更加简洁高效。同时,这也避免了重复获取周期计数可能带来的额外开销或潜在错误。
接下来,可以检查是否有其他类似的冗余逻辑需要优化,确保整个代码逻辑更加合理、高效。
DebugCollation花费了很多时间
现在至少可以看到,整体表现更加符合**帧率(frame rate)的情况,这点是好的。然而,仍然存在一些疑问,比如这些条形图(bars)**的大小为何发生了变化。接下来需要继续深入分析,找出问题的根本原因。
此外,现在也可以明显看出**调试数据整理(debug collation)占用了大量时间。这是可以理解的,因为事件数量过于庞大,导致处理时间过长。例如,在第一帧(first frame)时,事件数量达到了五十万(500,000)个,而后续帧虽然有所减少,但仍然有数万(tens of thousands)**个事件需要遍历。这么多数据的处理方式可能并不是最优的,导致了额外的性能消耗。
目前,可以看到一个更真实的性能情况(realistic picture),但仍然有很多可以优化的地方,比如如何更加高效地处理这些调试数据,以减少不必要的计算开销。
使用墙钟时间打印绘制一帧所需的时间
现在,希望能够利用之前获取的**“墙钟时间”(wall clock time)来进一步可视化帧时间(frame time)。一旦知道了每帧的实际耗时(seconds elapsed per frame),就可以将其绘制到调试界面中,以更直观地看到每帧的毫秒耗时(milliseconds per frame)**。
具体来说,在调试覆盖(debug overlay)部分,可以添加一些关于帧时间的信息。由于已经有了字体渲染相关的功能,因此可以直接在调试界面上绘制文本。例如,在绘制帧信息的地方,可以添加一行调试文本(debug text line),用于显示最新帧的时间信息。
目前时间有限,因此暂时不对每个帧单独绘制时间,而是先在**底部(bottom)**显示最近一帧的时间,后续可以再进行更完善的调整。
实现步骤如下:
- 访问调试状态(debug state),获取帧信息。
- 计算最新帧的墙钟时间差(wall seconds elapsed),得到该帧的耗时(单位:秒)。
- 将该值转换为毫秒(milliseconds),即秒数 × 1000。
- 在调试界面上绘制该值,以便可视化当前的帧耗时情况。
目前的实现可以正确显示帧时间(frame time),但默认取的是**第一帧(first frame)的时间,后续可能需要调整逻辑,以确保显示的是上一帧(last frame)**的时间。这一功能有助于更准确地了解帧率波动情况,为进一步优化提供数据支持。
很奇怪
不绘制调试矩形时,它降到91毫秒
如果不进行调试绘制(debug draw),仅仅绘制调试文本(debug text line),那么帧时间(frame time)是否会有所不同?当前的主要目的是观察帧时间(frame time),但可能应该显示的是**上一帧(last frame)**的时间,而不是默认的第一帧时间。
即使不进行绘制,**调试数据的整理(collation)**依然消耗了大量时间。调试整理(collation)的过程本身就非常昂贵,即使不渲染最终的结果,单单整理数据的过程也占用了相当多的计算资源。
如果关闭调试整理(collation),就会明显减少计算开销,但目前并不能完全确定是**调试整理(collation)本身导致的性能问题。可能的另一个影响因素是记录调试事件(recording debug events)**的开销。
为了进一步确认影响因素,可以尝试单独关闭调试整理(collation),以便观察性能变化。这有助于判断性能瓶颈究竟是在整理调试数据的过程中,还是在记录调试事件时产生的开销。
目前的问题是,如果不借助墙钟时间(wall clock time),就很难直观地判断调试整理的具体耗时。因此,下一步可以考虑使用墙钟时间来测量不同步骤的时间消耗,以找出性能优化的方向。
禁用调试事件记录,恢复到原来的性能
在**平台代码(platform code)**中,可以通过让 record_debug_event
不执行任何操作来测试其对性能的影响。具体方法是使用 #ifdef
预处理指令,将 timed_block
和 timed_function
相关的代码屏蔽,使其不再执行任何逻辑。
这样,帧标记(frame marker) 仍然可以正常工作,因为没有对其进行修改,而所有其他的**调试事件(debug events)都不会被记录。这种方式允许观察仅仅禁用调试事件记录(debug event recording)**后,程序性能的变化情况。
在游戏分析(game profiling)代码中,已经有相应的控制开关,可以直接关闭性能分析(profiling),但仍然保留帧标记(frame markers)。不过,是否要长期保持这种方式仍需进一步讨论。
关闭 record_debug_event
后,仍然无法直接得知**调试数据整理(collation)**的具体耗时,只能确认它是否有影响。因此,需要一种额外的测量方法,例如在 do_game_update()
过程中直接记录并打印帧时间(frame time)。
当前的主要问题是,如果不遍历所有调试记录(debug records),就无法得知帧时间,而帧时间的计算依赖于这些记录。因此,很难精准定位性能瓶颈,即究竟是**调试记录(debug recording)还是调试数据整理(collation)**占用了过多资源。
可能的解决方案包括:
- 在不遍历调试记录的情况下测量帧时间,比如在
do_game_update()
直接记录wall_clock_time
并输出到调试信息中。 - 基于帧计数(frame count)来控制分析代码的执行,比如只在
state.frame_count == 1
时执行某些性能测量,以减少干扰。 - 在
do_game_update()
或其他适当位置增加额外的时间测量逻辑,以便更准确地对比**调试记录(debug recording)和调试数据整理(collation)**的开销。
下一步需要尝试不同的测试方式,以明确性能开销的具体来源,从而针对性地优化调试系统,提高整体运行效率。
我们整理调试记录的过程非常耗时
目前可以明显看出,调试数据整理(correlation) 确实是导致性能问题的主要原因。尽管尚未明确打印出具体的时间开销,但当前的帧率非常稳定,说明记录调试信息(recording debug info) 并不会直接导致性能下降,而整理这些信息(correlating debug info) 才是主要的性能瓶颈。
这意味着,当前的数据整理方式存在优化空间。虽然初次尝试时可能无法精准判断某种方法是否合适,但经过实践后,能够更清楚地了解问题的核心。例如,翻译单元(translation unit) 相关的复杂性在一定程度上增加了调试的难度,不过通过实践积累经验,可以更好地规避类似问题。
目前,调试信息的传输已经趋于稳定,接下来的重点是:
- 优化调试数据的整理方式,减少不必要的计算开销,提高执行效率。
- 改进 UI 交互体验,让数据的可视化更加直观,使其更方便分析和导航。
- 深入挖掘调试数据,找出关键的性能瓶颈,以便更精准地优化整体系统性能。
虽然这个优化过程经历了一些波折,甚至浪费了一定的时间,但最终仍然控制在合理范围内,这也得益于团队协作和外部反馈,使得问题的解决速度加快。接下来,可以集中精力在优化数据整理方式和改进 UI 交互上,以进一步提升调试工具的实用性和性能。
你最喜欢的bug是什么?
在谈及最喜欢的程序错误(bug) 时,虽然可能确实存在某个特别有趣或印象深刻的 bug,但由于没有记录下来,因此难以回忆起具体的内容。尽管希望能回想起来并分享,但一时间却无法确定哪个是最值得一提的 bug。
在调试时,我常常不得不阻止自己不由自主地随机修改代码,因为懒惰,想着“希望是一个偶数个符号错误”。你有没有这种冲动?如果有,随着经验的增加,它是否有所减弱?
在调试时,经常会有一种随意更改代码的冲动,希望能通过随机调整来快速找到问题的根源。然而,随着编程经验的增加,这种冲动会逐渐减少,原因在于盲目修改代码通常会带来更多的问题,最终还是需要回头重新排查,甚至可能让问题变得更加复杂。
如果对软件质量有较高要求,就会认识到这种做法往往是得不偿失的。许多情况下,随意更改某个地方可能会暂时掩盖问题,但并没有真正解决它。
随着经验的积累,思维方式也会有所转变:
- 实验性修改仍然是调试过程中非常重要的一部分,但不能停留在“改动后问题似乎消失了”这个层面。
- 需要深入分析:“为什么这个改动能够修复问题?”
- 通过进一步验证,确保改动不是简单地掩盖了 bug,而是真正解决了其根本原因。
曾经有一次,在解决某个 bug 时,初步修改后看似问题已经解决,但始终觉得有些不对劲。在休假期间,这种疑虑一直存在,回到工作后进一步排查,最终发现真正的 bug 其实是另一个隐藏的问题,而之前的改动只是掩盖了真正的错误。
所以,编程过程中,直觉式调试并不是坏事,但不能让它成为问题排查的终点。找到一个有效的修改方案只是第一步,关键是要深入分析修改的原因,并通过实验验证其正确性,只有这样才能真正提高代码质量,并避免日后出现更多潜在问题。
在游戏需要调试之前,提前做这样的调试器,这与“按需编写代码”的哲学是否相违背?
当前的性能分析和调试工具并不违背**“按需编写代码”的理念。实际上,我们早已在渲染系统中编写了性能计数器,但目前仍然对游戏的时间消耗情况缺乏清晰的认知**。如果没有这种调试工具,我们的状态就像在被遮挡的挡风玻璃后开车,完全缺乏对系统运行状况的感知。
因此,性能分析和调试工具不是未来才会有用的东西,而是现在就必须具备的功能。我们的理念并非简单地“按需编写代码”,更准确的描述应该是**“在明确需求时编写代码”**。当确切知道需要某个功能时,就应该立即实现,而不是等到出现紧急情况才匆忙补充。
尽管这个工具当前可能不会直接影响游戏功能,但它的长期价值毋庸置疑。如果未来一定要编写调试工具,那么越早实现就能越早受益,而不是等到问题积累得难以处理时才手忙脚乱地添加。
调试工具的投入能够在开发的每个阶段持续提供价值,因此现在编写它不仅可以立即帮助分析性能问题,还能在整个开发过程中持续发挥作用。相比之下,如果拖到最后才实现,就浪费了前期所有可以利用的机会,同时也不得不花费时间去补足这部分功能。所以,从效率和开发体验的角度来看,尽早实现调试工具是更合理的选择。
有没有办法保持对旧代码的熟悉,还是频繁工作是唯一的方式?
对于如何保持对旧代码的熟悉,确实存在一定的挑战,尤其是当编写大量代码时,很难记住每一段代码的细节。在实际工作中,一天可能会写上千行代码,这意味着很难完全记住自己曾经写过的每一行代码。即便曾经很了解,随着时间的推移,很多细节都会被遗忘。因此,当需要重新回到这些旧代码时,通常会遇到一定的困惑和低效,可能会花费一两天的时间来理解曾经编写的复杂部分,这期间的产出会大幅减少。
为了尽量减少这种回归学习的时间,可以采取一些策略来使代码更容易理解。例如,在编写代码时,应该保持程序结构清晰,命名合理,尽量避免让代码变得复杂和混乱。如果某个函数的结构不够清晰,或者有许多特殊情况,应该花些时间在代码离开之前进行清理,使其更加简洁易懂。这样一来,回到这些代码时会更容易理解。
此外,编程中往往存在权衡取舍的情况。有时候,虽然当前代码看起来足够用,但如果知道自己可以做得更好,花些时间将代码写得更清晰、质量更高,可能在将来再次处理时会节省更多时间。因此,尽管短期内可能要花费一些额外的时间,但从长远来看,这种“提前做好”有时会带来更多的便利。通过不断的实践和经验积累,能够在这些权衡中做出更合适的选择。
然而,也要注意,如果过度优化或者过度设计代码,可能会造成不必要的浪费。为了避免这种情况,需要根据具体的需求判断是否值得投入额外的时间去优化代码。真正的难点在于,什么时候做得足够好,什么时候需要进一步提高,这往往需要经验的积累才能做出合适的决策。在编程过程中,这种权衡是常见的挑战,经验的积累会帮助做出更加精准的判断。
Mok实际上发现了bug,而AndreasK通过查看汇编代码弄清楚为什么会发生这个问题。编译器决定不在构造函数中内联调用,而是在析构函数中内联调用,导致起始标记错误,结束标记正确
在调试过程中,发现了一个 bug,问题的根本原因是编译器在处理构造函数和析构函数时,没有进行正确的内联操作。具体来说,编译器选择在构造函数中不进行内联,而在析构函数中却进行了内联。这导致了启动标记(start marker)不正确,而结束标记(end marker)则正确。通过检查汇编代码,找到了这个问题的根源。
当时,编译器并没有为构造函数和析构函数创建不同的例程,而是尝试共享相同的例程,这就引发了问题。理论上,如果函数被标记为静态函数,可能会解决这个问题,因为静态函数不会共享相同的例程。但目前还不确定为什么编译器会合并这两个不同的函数,导致出现这样的行为,这看起来像是一个奇怪的现象。甚至怀疑这是否可能是编译器的 bug,因为编译器允许将两个不同的函数合并成一个例程,这是不应该发生的。
总之,经过深入的分析,找出了导致问题的原因,这本来可能需要很长时间才能发现。
看起来那个FRAME_MARKER越过了这个TODO:“// TODO(casey):将这个移动到全局变量,以便在其下方可以有计时器?”
在调试过程中,发现一个问题,代码框架没有按预期移动,导致某些部分不在预定的范围内。这个问题可以通过使用哈希表来解决,这样就不会再出现这种情况。尽管问题本身不难修复,但这依然提醒了在设计时应避免依赖当前的结构,而应该考虑更合适的解决方案。
win32_game.cpp:将那个TODO移动并检查GlobalDebugTable是否存在
可以将某个逻辑放到代码的下方,并通过检查一个全局变量来处理。这种做法看起来是可行的。通过调整代码结构,将其移到合适的位置后,可以保证该逻辑正常工作。
你真的关心编译单元还是线程?哈希线程ID
目前并不关心复杂的单元或者线程的哈希值。编译时并不关注这些,我们只是在使用它们作为唯一的标识符。接下来打算做的是移除这些复杂的部分,直接使用文件名和行号来标识。这样做会更简单且符合实际需求,因此决定直接根据文件和行号来处理,而不是继续使用之前的复杂结构。
你提到过开发日志:你认为写开发日志对作者、读者还是对两者都有好处?
关于死锁日志的问题,目前并没有深入思考过它对写者或读者哪个更有利。没有特别的想法。
听起来调试器不仅仅是一个发现问题的工具,还能检测到可能会出问题的地方。这是一个正确的假设吗?我从未想到过调试器能保持“情境意识”。这个主意听起来不错
确实,调试代码不仅仅是用来找出问题发生的地方,还是为了在问题发生时能够及时检测到。这是调试代码的两个主要目的:一是帮助发现那些难以找到的错误,二是让我们能够知道当问题发生时,能够识别出问题所在。特别是性能问题,往往很难察觉,通常无法知道它们在哪里或者是什么原因,缺乏足够的情境意识。而内存问题也是类似的,很多时候可能并不知道游戏有什么问题,感觉可能只是因为帧率降低了,或者其他一些原因,实际上并非如此。因此,目标是通过合适的代码仪表化,让我们能够随时使用这些信息,虽然还需要一些时间才能达到这个目的,但我们在不断努力,最终希望能够让这些工具更加可靠地发挥作用。
相关文章:
游戏引擎学习第187天
看起来观众解决了上次的bug 昨天遇到了一个相对困难的bug,可以说它相当棘手。刚开始的时候,没有立刻想到什么合适的解决办法,所以今天得从头开始,逐步验证之前的假设,收集足够的信息,逐一排查可能的原因&a…...
05-02-自考数据结构(20331)- 动态查找-知识点
自考数据结构动态查找算法主要讲二叉树和平衡二叉树,但是感觉到了,就又续接了一部分,所以这篇备考的小伙伴着重看前两种就可以了。 知识拓扑 知识点介绍 二叉排序树(BST) 定义 二叉排序树(Binary Search Tree)又称二叉查找树,它或者是一棵空树,或者是具有下列性质的二…...
PyQt6实例_批量下载pdf工具_使用pyinstaller与installForge打包成exe文件
目录 前置: 步骤: step one 准备好已开发完毕的项目代码 step two 安装pyinstaller step three 执行pyinstaller pdfdownload.py,获取初始.spec文件 step four 修改.spec文件,将data文件夹加入到打包程序中 step five 增加…...
NVR接入录像回放平台EasyCVR视频融合平台城市/乡镇污水处理厂解决方案
一、方案背景 随着经济的快速发展和城市化的加快,城市污水排放量急剧增加,给城市环境和粮食安全带来了威胁。因此,污水处理厂的建设和高效运营成为对城市环境保护的重要任务。 目前,国内许多城市虽已建成污水处理系统࿰…...
Vue2 vs Vue3 生命周期全面对比:created 的进化与革新!!!
🎯 Vue2 vs Vue3 生命周期全面对比:created 的进化与革新 🔥 核心差异全景图 一、钩子函数命名与定位变化 1. 命名规范革新 Vue2 钩子Vue3 钩子 (Options API)Vue3 Composition APIbeforeCreate❌ 无setup() 替代created✅ 保留setup() 替代…...
Ubuntu 22.04安装MongoDB:GLM4模型对话数据收集与微调教程
在Ubuntu 22.04安装MongoDB Community Edition的教程请点击下方链接进行参考: 点击这里获取MongoDB Community Edition安装教程 今天将为大家带来如何微调GLM4模型并连接数据库进行对话的教程。快跟着小编一起试试吧~ 1. 大模型 ChatGLM4 微调步骤 1.1 从 github…...
并查集(Union-Find Set)课程笔记
目录 1. 并查集原理 2. 并查集的实现 3. 并查集应用 应用 1:省份数量问题 应用 2:等式方程的可满足性 1. 并查集原理 并查集用于处理需要将不同元素划分成若干不相交集合的问题。最开始时,每个元素都是单独的一个集合,随后根…...
算法刷题记录——LeetCode篇(1.4) [第31~40题](持续更新)
更新时间:2025-03-29 算法题解目录汇总:算法刷题记录——题解目录汇总技术博客总目录:计算机技术系列博客——目录页 优先整理热门100及面试150,不定期持续更新,欢迎关注! 32. 最长有效括号 给你一个只包…...
【区块链安全 | 第十四篇】类型之值类型(一)
文章目录 值类型布尔值整数运算符取模运算指数运算 定点数地址(Address)类型转换地址成员balance 和 transfersendcall,delegatecall 和 staticcallcode 和 codehash 合约类型(Contract Types)固定大小字节数组&#x…...
一款超级好用且开源免费的数据可视化工具——Superset
认识Superset 数字经济、数字化转型、大数据等等依旧是如今火热的领域,数据工作有一个重要的环节就是数据可视化。 看得见的数据才更有价值! 现如今依旧有多数企业号称有多少多少数据,然而如果这些数据只是呆在冷冰冰的数据库或文件内则毫无…...
android gradle一直编译不下来,可能是打开了gradle离线模式
gradle离线模式 当然,如果本地已经将gradle,lib都下载下来了,也可以打开这个离线模式,不然重启AS的时候可能会重新走一次下载流程...
(C语言)学生信息表(学生管理系统)(基于通讯录改版)(正式版)(C语言项目)
1.首先是头文件: //student.h //头文件//防止头文件被重复包含#pragma once//宏定义符号常量,方便维护和修改 #define ID_MAX 20 #define NAME_MAX 20 #define AGE_MAX 5 #define SEX_MAX 5 #define CLA_MAX 20 //定义初始最大容量 #define MAX 1//定义结…...
【Linux】Linux 系统启动流程详解
1. BIOS/UEFI 阶段 硬件自检(POST) BIOS/UEFI 执行硬件检查(内存、CPU、外设等)。若硬件异常,通过蜂鸣码或屏幕提示错误。 选择启动设备 按配置顺序(硬盘、U盘、网络等)寻找可引导设备。BIOS&a…...
Jetson 设备卸载 OpenCV 4.5.4 并编译安装 OpenCV 4.2.0
一、卸载 OpenCV 4.5.4 清除已安装的 OpenCV 库 sudo apt-get purge libopencv* python3-opencv # 卸载所有APT安装的OpenCV包:ml-citation{ref"1,3" data"citationList"}sudo apt autoremove # 清理残留依赖:ml-citation{ref"1,4"…...
【计算机网络】OSI七层模型完全指南:从比特流到应用交互的逐层拆解
OSI模型 导读一、概念二、模型层次结构2.1 物理层(Physical Layer)2.2 数据链路层(Data Link Layer)2.3 网络层(Network Layer)2.4 传输层(Transport Layer)2.5 会话层&…...
渗透测试:登录页面的测试-弱口令思路和实战
渗透测试:登录页面的测试思路和实战 渗透测试(Penetration Testing),也称为“渗透性测试”,是一种评估计算机系统、网络或Web应用安全性的一种方法。它通过模拟真实世界中的攻击手段和策略,来检测目标系统…...
Android BottomNavigationView 完全自定义指南:图标、文字颜色与选中状态
1. 核心功能概述 通过 Material Design 的 BottomNavigationView,你可以轻松实现以下自定义: ✅ 动态切换选中/默认图标 ✅ 自定义选中与默认文字颜色 ✅ 控制文字显示模式(始终显示/仅选中显示/自动隐藏) ✅ 添加动画和高级样…...
提示词工程
参考网站:提示工程指南 – Nextra 声明:我现在也才刚刚开始学习 人工智能,我会着重于 agent 的学习,如果有不对的地方请大家及时指出。 模型设置 前言 在向大模型发送请求时,常常能看到以下参数: {&qu…...
分页查询原理与优化方案完全指南
分页查询原理与优化方案完全指南 一、分页查询基础原理 1.1 传统分页实现方式 分页查询的核心目的是将大数据集分割成多个小块进行展示,最常见的实现方式是使用LIMIT-OFFSET语法: -- 基础分页查询 SELECT * FROM table_name ORDER BY id LIMIT page_size OFFSET (page_n…...
嵌入式软件设计规范框架(MISRA-C 2012增强版)
以下是一份基于MISRA-C的嵌入式软件设计规范(完整技术文档框架),包含编码规范、安全设计原则和工程实践要求: 嵌入式软件设计规范(MISRA-C 2012增强版) 一、编码基础规范 1.1 文件组织 头文件保护 /* 示…...
课程6. 决策树
课程6. 决策树 决策树直觉模型结构几何解释决策树的构建ID3算法信息内容标准使用决策树处理差距推广到回归问题分支标准与经典损失函数的关系 过度拟合和欠拟合欠拟合过拟合 优点和缺点案例随机生成数据集分类IRIS 数据集解决回归问题的一个简短例子 决策树 今天我们继续探索一…...
【UE5.3.2】初学1:适合初学者的入门路线图和建议
3D人物的动作制作 大神分析:3D人物的动作制作通常可以分为以下几个步骤: 角色绑定(Rigging):将3D人物模型绑定到一个骨骼结构上,使得模型能够进行动画控制。 动画制作(Animation):通过控制骨骼结构,制作出人物的各种动作,例如走路、跳跃、打斗等。 动画编辑(Ani…...
OpenCV 图形API(4)内核 API
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 G-API 背后的核心理念是可移植性——使用 G-API 构建的流水线必须是可移植的(或者至少具备可移植的能力)。这意味着&…...
pom.xml与.yml,java配置参数传递
pom.xml与 .yml java配置参数传递 在Java项目中,通过 pom.xml 和 .yml 文件(如 application.yml)传递变量通常涉及 构建时(Maven)和 运行时(Spring Boot)两个阶段的配置。以下是具体的实现方法&…...
LeetCode算法题(Go语言实现)_21
题目 给你一个整数数组 arr,如果每个数的出现次数都是独一无二的,就返回 true;否则返回 false。 一、代码实现 func uniqueOccurrences(arr []int) bool {freq : make(map[int]int)// 统计每个数字的出现次数for _, num : range arr {freq[n…...
Docker部署前后端分离项目
镜像下载 在有网络的电脑下载镜像(Windows):依次在CMD命令台执行以下代码 docker pull node:20docker pull openjdk:22-jdkdocker pull mysql:8.0docker pull nginx:1.27 删除镜像代码: docker rmi node:latest 查看镜像列表…...
Linux系统安装MySQL 8.0完整指南(新手友好版)
MySQL作为最流行的开源关系型数据库之一,广泛应用于各种开发和生产环境。本教程将详细介绍在Linux系统上安装MySQL 8.0的全过程,包括卸载旧版本、安装新版本、基础配置和远程连接设置,特别适合Linux新手学习使用。 一、卸载旧版MySQL&#x…...
第二次作业
#创建表,把id设为主键 mysql> create table test02(-> id int primary key, #----主键约束-> name varchar(50)-> ); Query OK, 0 rows affected (0.02 sec) #插入数据测试 mysql> insert into test02 values(1,"成都"); Query OK, 1 r…...
AI大模型下传统 Spring Java工程开发的演进和变化方向
1. 背景和动因 传统Spring开发优势:Spring生态以稳定、模块化、依赖注入(DI)等特性著称,长期支撑企业级应用开发,具备高扩展性和可维护性。AI大模型崛起:近几年,LLM(如GPT-4、LLaMA…...
周学习总结
这周继续学习了Java的知识点,还写了考查递归、递推与贪心的算法题。 算法小结 递归与递推一般是观察观察题干,分析题目的规律,可能还会用到分治算法,推导出一个合理的表达式,再使用函数递归来进行求解。 贪心在求解时…...
19.思科路由器:OSPF协议引入直连路由的实验研究
思科路由器:OSPF协议引入直连路由的实验研究 一、实验拓扑二、基本配置2.1、sw1的配置2.2、开启交换机三层功能三、ospf的配置3.1、R1的配置3.2、R2的配置3.3、重启ospf进程四、引入直连路由五、验证结果随着互联网技术的不断发展,路由器作为网络互联的关键设备,其性能与稳定…...
Zcanpro搭配USBCANFD-200U在新能源汽车研发测试中的应用指南(周立功/致远电子)
——国产工具链的崛起与智能汽车测试新范式 引言:新能源汽车测试的国产化突围 随着新能源汽车智能化、网联化程度的提升,研发测试面临三大核心挑战:多协议融合(CAN FD/LIN/以太网)、高实时性数据交互需求、复杂工况下…...
JSON的基础知识
文章目录 前言json协议的基本格式json 数组类型 的语法规则json协议报文的实例json常见的一些格式错误在gd32中使用cjson库小结 前言 json协议在互联网应用,物联网应用中都会用到。所谓工欲善其事必先利其器,我们需要学习了解json协议的具体格式…...
week2|机器学习(吴恩达)学习笔记
一、多维特征 1.1、什么是多维特征? 1)在我们的原始估计房价的版本中,我们只有一个变量: x x x 来预估 y y y 2)但是现在假设你也知道其他的参数变量,那么我们就可以引入多个参数来提高预测 y y y的准确…...
各类神经网络学习:(七)GRU 门控循环单元(上集),详细结构说明
上一篇下一篇LSTM(下集)GRU(下集) GRU(门控循环单元) 它其实是 R N N RNN RNN 和 L S T M LSTM LSTM 的折中版,有关 R N N RNN RNN 和 L S T M LSTM LSTM 请参考往期博客。 实际应用要比 …...
uniapp利用第三方(阿里云)实现双人视频/音频通话功能(附完整的项目代码)
要在UniApp中利用阿里云实现双人视频/音频通话功能,你需要使用阿里云的实时音视频服务(RTC)。以下是一个基本的实现步骤和示例代码。 基本的操作步骤 注册阿里云账号并开通RTC服务: 访问阿里云官网,注册账号并开通RTC服务。 获取AppID和AppKey: 在RTC控制台创建应用,…...
wsl2的centos7安装jdk17、maven
JDK安装 查询系统中的jdk rpm -qa | grep java按照查询的结果,删除对应版本 yum -y remove java-1.7.0-openjdk*检查是否删除 java -version 下载JDK17 JDK17,下载之后存到wsl目录下(看你自己)然后一键安装 sudo rpm -ivh jd…...
Android 单例模式全解析:从基础实现到最佳实践
单例模式(Singleton Pattern)是软件开发中常用的设计模式,其核心是确保一个类在全局范围内只有一个实例,并提供全局访问点。在 Android 开发中,单例模式常用于管理全局资源(如网络管理器、数据库助手、配置…...
Redis GEO
Redis GEO 引言 Redis GEO是Redis数据库中的一种高级功能,允许用户存储地理位置信息并执行基于地理空间查询的操作。本文将详细介绍Redis GEO的基本概念、使用方法以及在实际应用中的优势。 基本概念 GEO编码 GEO编码是指将地理位置信息(如经纬度&a…...
vulnhub-serile靶机通关攻略
下载地址:https://www.vulnhub.com/entry/serial-1,349/ 靶机安装特殊,附带安装参考文章:https://zhuanlan.zhihu.com/p/113887109 扫描IP地址 arp-scan -l扫描端口 nmap -p- 192.168.112.141访问80端口 线索指向cookie cookie是base64编…...
SAP-ABAP:OData 协议深度解析:架构、实践与最佳应用
OData 协议深度解析:架构、实践与最佳应用 一、协议基础与核心特性 协议定义与目标 定位:基于REST的开放数据协议,标准化数据访问接口,由OASIS组织维护,最新版本为OData v4.01。设计哲学:通过统一资源标识符(URI)和HTTP方法抽象数据操作,降低异构系统集成复杂度。核心…...
408 计算机网络 知识点记忆(3)
前言 本文基于王道考研课程与湖科大计算机网络课程教学内容,系统梳理核心知识记忆点和框架,既为个人复习沉淀思考,亦希望能与同行者互助共进。(PS:后续将持续迭代优化细节) 往期内容 408 计算机网络 知识…...
java学习笔记10——集合框架
枚举类的使用 Collection接口继承树 Map接口继承树 Collection 接口方法 总结: 集合框架概述 1.内存层面需要针对于多个数据进行存储。此时,可以考虑的容器有:数组、集合类2.数组存储多个数据方面的特点:> 数组一旦初始化,其长度就是确定的…...
埃文科技企业AI大模型一体机——昇腾体系+DeepSeek+RAG一站式解决方案
面对企业级市场海量数据资产与复杂业务场景深度耦合的刚需,埃文科技重磅推出基于华为昇腾算力DeepSeek大模型的企业一体机产品,提供DeepSeek多版本大模型一体机选择,为企业提供本地昇腾算力DeepSeek大模型RAG知识库的一体化解决方案ÿ…...
蓝桥杯---BFS解决FloofFill算法1---图像渲染
文章目录 1.算法简介2.题目概述3.算法原理4.代码分析 1.算法简介 这个算法是关于我们的floodfill的相关的问题,这个算法其实从名字就可以看出来:洪水灌溉,其实这个算法的过程就和他的名字非常相似,下面的这个图就生动的展示了这个…...
个人博客网站从搭建到上线教程
步骤1:设计个人网站 设计个人博客网站的风格样式,可以在各个模板网站上多浏览浏览,以便有更多设计网站风格样式的经验。 设计个人博客网站的内容,你希望你的网站包含哪些内容如你的个人基本信息介绍、你想分享的项目、你想分享的技术文档等等。 步骤2:选择开发技术栈 因…...
【FreeRTOS】裸机开发与操作系统区别
🔎【博主简介】🔎 🏅CSDN博客专家 🏅2021年博客之星物联网与嵌入式开发TOP5 🏅2022年博客之星物联网与嵌入式开发TOP4 🏅2021年2022年C站百大博主 🏅华为云开发…...
力扣每日一题:2712——使所有字符相等的最小成本
使所有字符相等的最小成本 题目示例示例1示例2 题解这些话乍一看可能看不懂,但是多读两遍就明白了。很神奇的解法,像魔术一样。 题目 给你一个下标从 0 开始、长度为 n 的二进制字符串 s ,你可以对其执行两种操作: 选中一个下标…...
Java EE(17)——网络原理——IP数据报结构IP协议解析(简述)
一.IP数据报结构 (1)版本:指明协议的版本,IPv4就是4,IPv6就是6 (2)首部长度:单位是4字节,表示IP报头的长度范围是20~60字节 (3)8位区分服务:实际上只有4位TOS有效,分别是最小延时,最…...
Pycharm运行时报“Empty suite”,可能是忽略了这个问题
问题:使用Pycharm运行testcases目录下的.py文件,报“Empty suite”,没有找到测试项。 排查过python解释器、pytest框架安装等等,依然报这个错,依然没找到,最后终端运行: pytest test_demo.py&a…...