IT评测·应用市场-qidao123.com

标题: 游戏引擎学习第158天 [打印本页]

作者: 杀鸡焉用牛刀    时间: 2025-3-15 08:25
标题: 游戏引擎学习第158天
回顾和本日的筹划

我们在这里会实时编码一个完整的游戏,没有使用引擎或库,一切都由我们自己做所有的编程工作,游戏中的每一部分,无论需要做什么,我们都切身实现,并展示如何完成这些任务。本日,我们正在处置处罚资产体系的末了一部分——内存管理。昨天,我已经简要介绍了一下关于这个资产体系的一些内容,本日我想简单地实现它,让大家可以或许看到最基本的实现方式。之后,我们会逐步过渡到更复杂的、适合实际发布版本的实现方式。
使用操作体系的假造内存体系解决我们的内存管理题目

本日我们将讨论如何使用假造内存来解决内存管理题目,特殊是在游戏的地点空间方面。之前在直播前有观众提到,是否可以通过假造内存来解决这个题目,我的答复是,如果你想要一个64位的地点空间来运行游戏,这是完全可行的,但如果不是这样,大概会面临一些题目,比如假造页表的空间不足,尤其是在32位体系下,这个题目会更为明显。Windows在32位体系中另有一些其他的题目需要考虑,这大概让假造内存的使用变得不那么抱负。
至于是否必须发布一个32位版本的 game,我们目前还没有决定。我并不想断言“不要做32位版本”,因为当我们准备发布时,我们可以根据需要做出选择。如果决定支持32位版本,我们固然可以调整代码架构来实现这一点。如今,我们的代码并没有被架构成让这个操作变得不大概,所以即使我们目前主要开发64位版本,也可以在以后再考虑是否需要支持32位。
值得注意的是,本日,很多游戏开发者选择只支持64位操作体系的游戏,因为这种做法是完全可行的,而且大概会赚到不错的收入。然而,另一方面,这样做也大概会导致我们失去一部分玩家群体,因为有15%的Steam用户仍然使用32位操作体系。所以这是一个需要考虑的题目。
我想展示一种方法,先做一个简单版本的体系,大家可以看到基本思路。这个简单版本是基于64位内存空间的,肯定可以或许在64位体系上运行,但在32位体系上大概表现不好。接下来,我可以展示如何使用假造内存相干的API(如 VirtualAlloc 和 VirtualFree)来实现这一点,这些操作非常直接,大概本日我们就可以做这个,因为如我所说,我们需要先实现一个简单的版本。
目前,资产体系的内存已经被严格限定了,因为它并没有实现假造内存的管理功能。当我们尝试实例化英雄角色时,程序会立即触发一个断言,提示资产体系内存不足。这是因为在加载英雄资产时,尝试执行push size操作时,内存空间已经满了,导致无法继续分配内存。因此,接下来我们需要想办法解决这个题目。
跟踪内存负载

题目是,我们需要开始考虑如何检测是否出现内存不足的情况。如果发现内存不足,我们就需要进行资产驱逐,比如删除一些旧的资产,以确保始终有富足的内存来加载我们即将需要的新资产。这样做可以确保在游戏运行过程中,始终可以或许保证内存空间富足,克制因内存不足而导致的崩溃或性能题目。
使用一种分配方案,从操作体系获取内存并在驱逐时归还

我们可以开始更改为一种分配方案,通过操作体系获取内存,并在每次加载资产时将内存开释回操作体系。这种方式非常简单,可以通过手动平台API实现,特殊是在64位操作体系上,这个方法应该不会有太多题目。假造内存分配应该在Windows上运行得相当快,我们固然可以进行性能测试来确认这一点。如果我们希望实现这一点,我们可以让平台API具备从操作体系获取内存和将内存返回操作体系的能力。
我们可以为此界说两个函数,分别是platform_allocate_memory和platform_deallocate_memory。这些函数的实现非常简单,platform_allocate_memory会吸取一个巨细参数,并返回一个内存指针,而platform_deallocate_memory则是标准的内存开释操作。
当我们有了这两个函数后,我们还可以进行更多的修改。比如,后期我们可以将内存管理改为按需增长,即不需要一开始就分配所有的内存,而是允许游戏在运行时动态增长内存。这也非常简单,只需要进行少量修改,代码不会有太大的变更。
这些功能并不是仅仅为了调试,我们可以将它们作为实际的操作平台调用。在游戏发布时是否允许这种动态内存管理,我们还没有决定,但无论如何,这种方式是可行的,并且实现起来非常直接。
总的来说,通过这些平台调用,我们可以非常方便地从操作体系申请和开释内存。这种实现方式非常简单,险些没有复杂的部分。如果你跟随手工英雄的历程,你应该已经很清楚如何使用这些API,这只是一个简单的示范,目的是展示如何灵活地管理内存。


实现 platform_allocate_memory

我们可以通过在平台API中添加platform_allocate_memory和platform_deallocate_memory函数来管理内存。这些函数将分别调用操作体系的VirtualAlloc和VirtualFree函数来分配和开释内存。这两者的实现都非常简单,只需调用操作体系提供的API即可。
在实现platform_allocate_memory时,我们向操作体系哀求分配指定巨细的假造内存。操作体系会根据传入的巨细进行分配,成功后返回一个指向分配内存的指针。如果分配失败,返回null指针,调用者会知道无法从操作体系获取更多内存,处置处罚这种情况就可以了。
对于platform_deallocate_memory,它的工作就是开释已经分配的内存。VirtualFree会根据传入的指针来开释内存,操作非常简单。虽然我们可以验证一下VirtualFree是否允许传入空指针,但这并不影响功能的实现。
这些操作的实现方式非常基础,险些不需要任何复杂的处置处罚。我们只需要确保在平台API表中参加allocate_memory和deallocate_memory的界说。然后,我们就可以随时通过这些接口分配和开释内存,整个过程非常直接。
通过这种方法,我们不需要做任何复杂的内存管理操作,内存的分配和开释都由操作体系来处置处罚。这样做的利益是,我们不需要手动管理内存的分配和回收,只需依靠操作体系提供的内存管理功能。


这些厘革破坏了循环实时代码编辑功能

为了处置处罚内存管理题目,特殊是如安在加载图像时确保富足的内存可用,需要考虑一些关键因素。当前在调用 load bitmap 时,内存不足的题目暴暴露来。此时,不能简单地在 load bitmap 过程中直接开释内存,因为该函数是在帧的处置处罚中被调用的,而这些帧大概正在使用某些已经加载的位图资源。这样如果在处置处罚中随意开释内存,大概会导致程序堕落或出现资源冲突。
因此,需要确保有一种安全的方式来开释内存,以便加载新的位图。为此,无法仅依靠 load bitmap 函数自己来进行内存开释,因为它运行在帧的处置处罚中,开释内存大概会影响当前正在使用的资源。相反,必须在更符合的机遇,保证开释的内存不会影响正在使用的资源。
两种方法:a) 将 LoadBitmap 调用推迟到帧结束时,b) 保持一定的空闲空间,确保加载始终大概

为了应对内存不足的题目,提出了几种解决方案。一种方法是缓冲所有加载位图的哀求,这样可以在需要时批量处置处罚加载哀求。另一种方法是始终保持一定量的内存空闲,这样就可以或许确保在分配内存后,可以稍后再进行内存开释。
在当前的简化实现中,采取了不设置硬性内存限定的策略,而是使用软性限定来解决这个题目。软性限定意味着不会立即强制限定内存的使用,而是确保在需要加载新资源时有富足的内存空间,具体的内存管理将稍后处置处罚。这样做的目的是简化当前的实现,同时克制因硬性内存限定而导致不必要的复杂度。
跟踪资产体系中使用的内存量 (AcquireAssetMemory)

为了追踪实际使用的内存,决定在资产加载时记载每次分配的内存量。首先,初始化总内存使用量为0。每次加载资产时,会通过一个函数来处置处罚内存分配,并且盘算分配的内存巨细。
具体操作是,在加载资产时,调用AcquireAssetMemory函数,该函数吸取资产数据和所需的内存巨细。然后执行内存分配,成功后会将所分配的内存巨细加到总内存使用量中。最终,只有在内存成功分配后,才会增长内存使用量。
这个过程的关键是通过调用AllocateMemory函数分配内存,并根据实际分配的内存巨细更新内存使用情况,从而可以正确追踪整个资产体系的内存斲丧。



RealeaseAssetMemory 需要我们提供要开释的资产巨细

我们需要进行内存开释,因此会调用开释资产内存的函数,并将内存归还给体系。为了实现这一点,我们会调用一个开释函数,并传递一个 void 指针。但题目在于,我们需要知道这块内存的巨细,因此必须在开释函数中额外传递内存巨细参数。虽然这样做有些别扭,但我们必须跟踪内存的巨细。因此,我们会直接将巨细参数传入开释函数。
调用该函数后,它将执行 platform_release_memory,实际上是 DeallocateMemory,并传入要开释的内存地点。前提是这块内存地点非零(即有效)。此外,我们还需要减少 assets.TotalMemoryUsed 的值,减去被开释的内存巨细。
当前的实现不会涉及多线程,因此不必考虑并发安全题目。但是,如果将来我们筹划从多个线程调用该函数,就需要使用原子加 (atomic add) 或原子减 (atomic decrement) 来确保操作的线程安全性。目前临时没有多线程调用的筹划,所以可以不考虑这个题目。但需要记取,一旦在多线程情况下使用该函数,现有实现就会存在安全隐患。比方,如果 LoadAssetWork 开始涉及并发操作,我们就必须考虑线程安全的题目。
此外,另有一个题目需要注意。当前的实现并未正确处置处罚文件流的情况,即在开释内存时,并不会处置处罚文件流导致的内存残留。因此,我们需要确保无论如何都正确开释文件流占用的内存。实际上,即使文件读取失败,也应该进行某种操作,比方填充一个无效的数据块,或者将内存区域清零。这样做更加安全,并能克制潜在的题目。
关于文件读取失败后的处置处罚,我们大概会填充一块无效数据,比方全部置零。检查代码后发现,已经有一个 ZeroSize 相干的方法,因此可以使用它来清空内存。
在 platform 层面,我们有一个内存分配的函数,同时也有一个内存开释的函数。我们需要在正确的位置调用它们,以确保体系资源得到合理管理。

使用平台调用取代内存区域

我们可以实现一种机制,使内存的分配和开释更加高效。具体来说,当需要分配位图内存时,不再使用 PushSize,而是调用 AcquireAssetMemory,并将 game_assets 以及所需的内存巨细作为参数传入。同样的,在声音资源的分配过程中,也可以使用 AcquireAssetMemory,传递 Assets 和所需的巨细MemorySize,而不是 PushSize。
这样一来,游戏运行时会直接从操作体系获取内存,整个流程变得更加流通,克制了之前大概存在的题目。然而,目前仍然存在一个题目,即内存的分配和回收仍然只是简单地获取和开释指定巨细的内存,并没有到达抱负的状态。因此,需要继续优化和美满这个流程,以确保资源管理的合理性和高效性。



跟踪内存使用情况,并在帧结束时开释内存 (EvictAssetsAsNecessary)

我们需要检查当前正在使用的资产内存总量,并在其过高时主动开释部分内存。为此,需要在一个符合的时间点执行该操作,以确保不会影响正在使用的资产。
一个抱负的机遇是在每一帧结束时,也就是所有临时内存都已开释、所有资源清理完毕的时候。在这一时刻,可以让资产管理体系检查当前的内存占用情况,并开释一些不再需要的资产,使其回到合理的工作集巨细。
可以在 game.cpp 里实现这一逻辑,在帧结束时调用 FreeAssetsAsNecessary 或 EvictAssetsAsNecessary,并传入 TranState->Assets 作为参数。这样就能确保在固定时间点执行清理,克制在多线程情况下进行不必要的壅闭操作。通过这种方式,可以确保资产体系有一个独立的时间段来回收不必要的内存。
EvictAssetsAsNecessary 的具体实现将包罗一个循环逻辑,该循环会一连检查 TotalMemoryUsed 是否超过了设定的 TargetMemoryUsed(即目的内存占用阈值)。如果当前占用的内存超出了目的值,就尝试开释某个资产,直到总占用内存降至合理范围内。
如果可以或许成功开释资产,就继续循环;如果无法开释,则跳出循环,并触发错误处置处罚逻辑。因为理论上不会出现无法开释资产的情况,所以如果发生了,则分析程序存在 Bug,需要进一步调查和修复。
接下来,需要具体实现这一逻辑,以确保资产体系可以或许高效管理内存,克制过分占用体系资源。



驱逐近来最少使用的资产

我们需要找到近来最少使用(LRU)的资产,并开释其占用的内存。为此,需要一个可以或许跟踪资产使用情况的机制,比方一个用于管理资产槽(slot)的数据结构。可以实现一个 GetLeastRecentlyUsedAsset 函数,该函数返回最久未使用的资产槽索引。
在执行资产回收时,首先调用 GetLeastRecentlyUsedAsset 来获取最久未使用的资产槽索引。如果返回的索引不是 0(即该索引有效,指向某个可开释的资产),就可以进行回收。
接着,使用该索引找到对应的资产,并调用 EvictAsset 函数来开释该资产所占用的内存。EvictAsset 的作用是彻底移除该资产,使其不再被体系占用,并开释其相干资源。这一过程可以封装成 internal void EvictAsset(game_assets *Assets, uint32 SlotIndex),该函数负责从体系中移除该资产,使其彻底消失。
整个过程可以总结如下:
最终,EvictAsset 使资产彻底离开体系,不再占用资源,就像某个被排除在外的角色一样,它已不再属于当前的资源集合,必须被移除。

EvictAsset

EvictAsset 的作用是将资产从“已加载”状态转换为“已开释”状态。为了实现这一点,需要先确定资产槽(slot)的位置,然后检查该槽的状态,确保它确实处于“已加载”状态。
在资产管理体系中,并非所有状态的资产都可以被移除。比方,已锁定(locked)的资产无法被回收,而列队等候加载(queued)的资产也不应被移除。因此,如果尝试回收一个未处于“已加载”状态的资产,应该触发错误。
假设资产确实处于“已加载”状态,接下来的步骤就是将其状态转换为“未加载”并开释内存。实现方式是调用 ReleaseAssetMemory 来回收该资产的内存。
一个主要的题目是,开释内存时需要知道资产的具体巨细,但目前体系中并没有存储每个资产的巨细信息。因此,需要找到一个方法,使得在开释内存时可以或许轻松获取资产的巨细。
在当前体系架构下,获取内存指针相对简单,因为每个资产在其对应的槽中都有内存地点记载。但是,资产的巨细信息没有同一的管理方式,因此在回收内存时大概会遇到困难。为了简化内存管理流程,可以对资产槽(slot)结构进行改进,使内存管理更加规范化。
一个大概的优化方向是同一差别类型资产(如位图和音频)的内存管理方式。如果可以或许在资产槽中存储资产类型信息(如标记它是“位图”照旧“音频”),那么在开释时就可以通过这个信息来确定相应的巨细,而无需额外的处置处罚逻辑。
目前的题目在于,差别类型的资产大概存储方式差别。比方,在文件格式中,数据只是简单地存储在文件中,而别的的信息则是通过额外的盘算得到的。这种方式虽然有一定的灵活性,但在内存开释时会带来额外的复杂性。因此,需要权衡是否继续相沿这种方法,照旧调整存储结构,使内存管理更加规范和同一。
总体而言,需要做的优化包括:
需要进一步思考的是,如安在不增长太多额外开销的情况下,使整个资产管理体系更加高效和易维护。

在 AssetState 中区分位图和声音

为了更高效地管理资产,我们需要在资产槽(slot)中记载资产的类型,比方区分它是位图(bitmap)照旧音频(sound)。目前的体系中,并没有一个直接的方式来存储这个信息,而是在差别的地方进行判定和处置处罚。为了优化这一点,我们可以在资产状态(asset_state)中引入额外的标记位,使其既能表示当前的加载状态,又能区分资产类型。
一种方法是使用状态字段的高位来存储资产类型。比方:

这样,在检查资产状态时,可以直接屏蔽掉类型信息,仅关注加载状态。比方,通过 AssetState_Mask 获取资产的基础状态,而高位仍可用于资产类型辨认。这种方式的长处是:
在实现过程中,需要修改 uint32 GetState(asset_slot *Slot) 之类的函数,使其可以或许正确剖析状态字段。比方:

这样,在资产管理体系中,任何时候查看一个资产槽时,都能立即知道该资产是位图照旧音频,而不需要额外的盘算或存储。这种方法虽然有些“临时拼凑”(janky),但它能有效地完成任务,并使资产管理更加直观和高效。
最终,在卸载资产时,可以通过检查 AssetState_Loaded 确保资产处于可卸载状态,并使用新的类型信息正确地开释内存。这样,整个资产管理流程就更加清楚和同一了。


盘算资产占用的内存量

为了正确开释资产槽(slot)所占用的内存,我们需要盘算该槽实际占用的内存巨细,并开释相应的内存。因此,我们需要创建一个 GetSizeOfAsset 函数,该函数可以或许根据资产的类型(如位图或音频)来盘算其所需的内存量。
具体实现思路

优化点


最终效果

通过上述改进,我们可以正确地盘算每个资产槽的内存巨细,并在符合的机遇开释它们,从而更高效地管理游戏资产的内存使用,进步体系的稳定性和性能。


消除重复盘算

为了优化内存管理并减少代码重复,我们引入了 asset_memory_size 这一结构,旨在更高效地盘算和存储资产的内存信息。

核心优化点


具体代码调整


附加优化



最终效果




找到要开释的内存块的位置

我们通过 GetType(Slot) 来确定内存的存放位置,以便更合理地管理和开释内存。此外,为了优化 近来最少使用(LRU, Least Recently Used) 资产的查找,我们引入了一种简单的数据结构来追踪资产的访问顺序。

优化点

1. 通过 GetType(Slot) 同一内存位置判定





2. 计划 LRU 资产回收机制

题目:

解决方案:

数据结构:
  1. struct asset_memory_header {
  2.     asset_memory_header *Next;
  3.     asset_memory_header *Prev;
  4. };
复制代码

使用双向链表跟踪近来最少使用的资产

我们打算实现一个简单的双向链表,来管理和跟踪已加载的资产(比如声音或位图)。每当某个资产被使用时,我们将其移到链表的前端,这样链表末尾的节点就会不停是最久未使用的资产。这种做法非常简单,险些没有复杂的内容。
链表的构建和操作

实现的简要总结



双向链表理论 (黑板)

双向链表(Double Linked List)是一种非常实用的数据结构,它允许在列表中的元素前后进行快速操作。每个节点不仅包罗指向下一个节点的指针(next),还包罗指向上一个节点的指针(prev)。通过这种方式,每个节点都能知道自己前面的节点和后面的节点,提供了比单向链表(Single Linked List)更灵活的操作方式。
双向链表的结构


双向链表的长处


如何操作双向链表

与单向链表的区别


总结

双向链表是一个非常灵活且强大的数据结构,特殊适用于需要频繁插入、删除或双向遍历的场景。只管它的内存开销比单向链表要大,但它提供了更高效的操作,可以或许更方便地进行节点的移动、删除和插入。
AddAssetHeaderToList

在实现链表时,接纳了一个名为“哑元”(sentinel)的技术来简化插入操作。这个哑元头部是一个假造的节点,存在于链表的结构中,但并不指向任何实际的资产或数据。通过这种方式,我们能保证链表的头部始终有一个指针可以操作,从而克制了在处置处罚链表时的特殊边界情况。
哑元节点(Sentinel Node)的使用

通过这种方式,链表的插入和删除操作变得更加同一和简化,因为哑元节点保证了每次操作都能从一个稳定的出发点开始。

指针的语义设置

为了简化节点插入操作,我们通过调整链表中节点的 previous 和 next 指针,使得插入操作更加直观且便于实现。具体来说,在插入节点时,我们首先设置新节点的 previous 和 next 指针,使其指向当前节点前后的相应节点。然后,我们通过调整这些节点的指针,使得链表结构保持同等。
具体操作步骤

总结

这种方法通过设置新节点的前后指针,并让相邻节点的指针指向新节点,确保链表结构的同等性。这个过程可以通过简单的指针调整来完成,不需要额外复杂的逻辑操作。

RemoveAssetHeaderFromList

在处置处罚双向链表时,移除和插入资产头部(Asset Header)操作非常简便。我们需要做的只是调整相邻节点的指针,确保链表的连接不会中断。
移除资产头部(Remove Asset Header)操作

插入资产头部(Add Asset Header)操作

资产管理流程

改进和优化

关于 Sentinel 的使用


通过这些方法,整个资产管理流程变得更简洁高效,同时也能保证内存管理和资源回收的灵活性。


初始哨兵设置

在启动时,接纳了一个循环链表的结构,其中的 Sentinel 节点既是头节点,也是尾节点。该 Sentinel 节点的 previous 指针指向它自身,next 指针也指向它自身。这样一来,当插入新的节点时,链表的结构保持同等,不需要额外的检查。
Sentinel 节点的作用


内存分配和资产管理


资产回收机制


总体来看,使用 Sentinel 节点让链表的操作变得更加简单高效,克制了空链表的情况并减少了错误发生的大概性。在内存管理上,也通过随机逐出资产来控制内存的使用量,虽然这是一种简化的做法,但可以或许快速有效地保持体系在内存限定内。


我们应该克制驱逐被锁定的资产

在资产管理中,存在一种“锁定资产”的概念,这类资产是不允许被逐出的,因为它们正在被后台任务使用。为了确保不会在后台任务正在使用时错误地逐出这些资产,我们需要在将资产添加到链表时,检查它是否被锁定。如果是锁定资产,则需要克制将其添加到逐出队列中。
锁定资产的处置处罚


接下来的步骤


总结


双向链表的类型及其实现概述

在双向链表的实现中,存在两种主要的方式:带哨兵节点(Sentinel)不带哨兵节点(Non-Sentinel)
不带哨兵节点的链表

这种方式需要显式地界说链表的头指针(first)尾指针(last)。在这个链表中,第一个节点的前指针指向空(NULL),而末了一个节点的后指针也指向空(NULL)。此时,链表的第一个节点和末了一个节点需要特殊处置处罚,在插入和删除操作时需要不断检查这些指针是否为空,增长了代码的复杂性。
带哨兵节点的链表

哨兵节点方法简化了链表的管理。哨兵节点始终存在,并且永远不会被移除。无论链表的长度如何,哨兵节点始终作为链表的出发点和尽头。具体而言:

操作简化

通过使用哨兵节点,链表的第一个节点末了一个节点不再需要显式存储。它们可以通过访问哨兵节点的**后指针(first)前指针(last)**来隐式获取。哨兵节点使得每次添加或删除元素时,操作都是同等的,不需要额外的空值检查,因为链表始终有一个完整的节点结构。

总结

使用哨兵节点的双向链表通过简化链表的管理和减少空指针检查,使得链表操作更加简洁高效。通过保持链表的圆形结构,所有操作都可以视作在一个始终存在的结构上进行,克制了额外的判定逻辑,极大地简化了代码的复杂性。
哪个函数拥有指向链表头指针的所有权?

在链表的管理中,通常会有一个题目是“谁拥有链表头指针”的题目。这个题目意味着,需要明确哪个部分的代码或模块负责管理和维护链表的头指针。
链表头指针的所有权


管理头指针的责任


总结

“拥有链表头指针”意味着对链表的管理和控制,确保链表结构在操作中始终保持同等和有效。这个责任通常由特定的模块或函数来负担,确保链表的正确操作和内存管理。
如果你关心缓存,链表不是你总是被告知不要使用的吗?

在处置处罚链表时,通常会听到关于缓存友好的建议,尤其是当链表的数据量较大时。如果代码频繁访问链表,大概会遇到缓存未掷中(cache miss)的题目,这会导致性能下降。然而,在不确定代码是否会频繁操作链表时,过早优化缓存并不总是明智的做法。
链表和缓存的关系


是否优化链表的缓存性能?


总结

链表在某些场景下大概不适合处置处罚大规模、高频率的数据操作,但如果链表是当前最佳的选择,就不需要立即担心缓存题目。首先确保代码的正确性和简洁性,只有在性能成为瓶颈时,才需要考虑优化数据结构。
platform_allocate_memory 函数是否可以分配比哀求的更多的字节,并将巨细存储在那边,以克制需要将其传递给 free 函数?

平台的分配函数通常会分配比哀求的稍多的字节,并将额外的空间用于存储与该内存块相干的数据,以克制将其传递给开释函数。但这种做法并不总是抱负的,因为通常在分配时,已经知道需要的正确巨细,比方在某些情况下,已经明确了内存的需求,所以直接按照所需的巨细进行分配会更加简便。
在这种情况下,采取一种方法是在每个内存块的末尾附加一个列表头,克制了额外的内存管理复杂性。通过这种方式,可以直接受理内存块的巨细和其他元数据,而不需要额外的空间分配和复杂的指针操作。这种做法简化了内存分配过程,使得内存管理更加直接和高效。
在每个资产结构的末尾都有一个列表头,这样做会不会导致缓存大量失效,因为资产结构大概很大?

即使资产结构体大概很大,这种结构不会显著影响缓存,因为缓存是基于较小的内存块(cache line)进行优化的。所以,触及资产结构末尾的链接与触及其他部分的链接没有太大区别。要使这个链表结构更加适应缓存,可以采取的步伐是将链表的链接数据块会合处置处罚。具体来说,当前的结构是“资产数据 -> 链接 -> 资产数据 -> 链接”,如果要进步缓存效率,可以将这些链接数据放在一个单独的缓冲区中,这个缓冲区专门存储所有的链接,像是一个独立的区域存储链接(链接 -> 链接 -> 链接),这样每个缓存行可以包罗多个链接,从而进步缓存的掷中率。
在 RemoveAssetHeaderFromList 中,是否故意义将正在移除的头节点的 prev 和 next 指针清零,照旧这只是多余的清理?这样做有什么利弊?

在从链表中移除节点时,清除被移除节点的前驱指针并不是必须的操作,但为了调试方便,可以做一些额外的检查。比方,可以将被移除节点的 header next 和 header previous 设为零,这样就可以通过调试检查来确认是否出现了题目。这并不会影响性能,因为这个操作的频率通常较低。如果这个操作频繁发生,并且成为性能瓶颈,那么大概需要重新考虑使用链表结构,而选择其他更适合高频操作的数据结构。总的来说,进行这种额外的调试检查是没题目的,但要根据具体情况决定是否进行。

在实际游戏中是否会有一个“头颅喷泉”,大概作为万圣节的物品?

在实际游戏中,大概会有一个“喷泉的头”作为某种物品出现,或许它可以作为万圣节的特殊道具。这听起来是个不错的创意。
完成这个之后,你将如何重新启用实时代码重载功能?

重新启用实时代码重载其实非常简单,即使我们坚持当前的方案。只需让平台代码的循环实时编辑保存一组头文件,并且当进行保存操作时,将这些头文件写入磁盘即可。然而,我甚至建议可以考虑不这样做,而是在进行实时代码编辑时完全使资源缓存失效,这样我们就不需要存储瞬时内存区域。
至于内联函数,它们基本上是一种将函数的代码嵌入调用点的方法,这样可以减少函数调用的开销,尤其是在一些频繁调用的小函数中。通过这种方式,函数调用的指令被直接更换为函数体的代码,从而克制了调用栈和跳转的成本。但需要注意的是,过多使用内联函数大概导致代码膨胀,因为每次调用函数时都会嵌入一份副本。
你能简要讲解一下内联函数吗?

内联函数其实就是给编译器一个提示,告诉它这个函数很大概是小的,应该直接嵌入到调用它的地方,而不是通过传统的函数调用来执行。这样做的目的是希望通过内联优化进步性能,减少调用的开销。编译器收到这个提示后,大概会决定直接把函数的代码插入到调用位置,而不是产生额外的函数调用指令,从而优化代码执行的效率。
然而,需要注意的是,内联函数并不强制要求编译器一定要进行内联,它只是给编译器的一个建议。现代编译器会根据自身的判定来决定是否进行内联,只有在使用了强制内联(如使用__forceinline)时,才会强制要求编译器进行内联。所以,使用inline关键字并不意味着编译器一定会把函数进行内联,它依靠于编译器的优化决策。
此外,过分使用内联函数大概会导致代码膨胀,因为每个调用内联函数的地方都会嵌入该函数的完整代码,这样大概会增长代码的巨细和复杂性。因此,是否使用内联函数需要根据具体情况权衡使用。
你最喜好实现哪种经典数据结构?

在谈到实现数据结构时,最喜好的结构是单链表,因为它非常简单且实现起来很轻松,操作起来也很方便。有些操作,比如添加元素到链表中,甚至可以通过原子互换来完成,这让整个过程变得非常高效和有趣。移除元素时大概需要一些额外的原子互换操作,但总的来说,添加操作的简单性和高效性让单链表成为了一个非常吸引人的选择。
至于体系是否支持热加载的题目,虽然没有具体分析,但通常热加载指的是在运行时动态加载和更新代码或资源,而不需要重新启动体系或应用。如果体系的计划支持这种动态加载机制,那么它可以通过特定的机制来更新资源或功能,而不干扰当前运行的历程或服务。
这个体系是否/将来是否支持资产的热加载?

关于资产的热加载,体系自己并不支持这一功能,因为没有涉及到艺术家的工作,也没有相干的需求。所有的资产文件都是批量提供的,因此并不需要支持热加载。然而,如果有需要,也可以很容易地实现热加载功能。实现方法很简单,比方,可以为加载位图的代码添加功能,检查文件是否存在并从外部加载位图。体系已经有加载位图的代码,如果需要实现这一功能,只需要在加载过程中检查文件路径是否存在,然后从指定位置加载文件。只管目前没有这个需求,但如果需要热加载,实际上黑白常简单且明显的。
编写你自己的非壅闭动态分配器,而不是使用操作体系的内存体系,是否故意义?

讨论了使用自己的非壅闭动态分配器而不是依靠操作体系的内存体系。对于64位体系,操作体系的内存分配器大概富足使用,但在32位体系上使用大概会有些题目,因此有大概会选择实现自己的分配器。本日展示了如何让内存分配工作,但还未涉及内存结构部分。虽然目前没有决定最终的方案,但有很大的大概性会接纳自界说分配器,而不是依靠平台提供的分配器。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com (https://dis.qidao123.com/) Powered by Discuz! X3.4