回顾和本日的筹划
我们在这里会实时编码一个完整的游戏,没有使用引擎或库,一切都由我们自己做所有的编程工作,游戏中的每一部分,无论需要做什么,我们都切身实现,并展示如何完成这些任务。本日,我们正在处置处罚资产体系的末了一部分——内存管理。昨天,我已经简要介绍了一下关于这个资产体系的一些内容,本日我想简单地实现它,让大家可以或许看到最基本的实现方式。之后,我们会逐步过渡到更复杂的、适合实际发布版本的实现方式。
使用操作体系的假造内存体系解决我们的内存管理题目
本日我们将讨论如何使用假造内存来解决内存管理题目,特殊是在游戏的地点空间方面。之前在直播前有观众提到,是否可以通过假造内存来解决这个题目,我的答复是,如果你想要一个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),该函数负责从体系中移除该资产,使其彻底消失。
整个过程可以总结如下:
- 通过 GetLeastRecentlyUsedAsset 获取最久未使用的资产槽索引。
- 如果索引有效,则调用 EvictAsset(Assets, SlotIndex) 开释该资产。
- EvictAsset 负责清理该资产的所有相干数据,并开释内存,使体系资源得到回收。
最终,EvictAsset 使资产彻底离开体系,不再占用资源,就像某个被排除在外的角色一样,它已不再属于当前的资源集合,必须被移除。
EvictAsset
EvictAsset 的作用是将资产从“已加载”状态转换为“已开释”状态。为了实现这一点,需要先确定资产槽(slot)的位置,然后检查该槽的状态,确保它确实处于“已加载”状态。
在资产管理体系中,并非所有状态的资产都可以被移除。比方,已锁定(locked)的资产无法被回收,而列队等候加载(queued)的资产也不应被移除。因此,如果尝试回收一个未处于“已加载”状态的资产,应该触发错误。
假设资产确实处于“已加载”状态,接下来的步骤就是将其状态转换为“未加载”并开释内存。实现方式是调用 ReleaseAssetMemory 来回收该资产的内存。
一个主要的题目是,开释内存时需要知道资产的具体巨细,但目前体系中并没有存储每个资产的巨细信息。因此,需要找到一个方法,使得在开释内存时可以或许轻松获取资产的巨细。
在当前体系架构下,获取内存指针相对简单,因为每个资产在其对应的槽中都有内存地点记载。但是,资产的巨细信息没有同一的管理方式,因此在回收内存时大概会遇到困难。为了简化内存管理流程,可以对资产槽(slot)结构进行改进,使内存管理更加规范化。
一个大概的优化方向是同一差别类型资产(如位图和音频)的内存管理方式。如果可以或许在资产槽中存储资产类型信息(如标记它是“位图”照旧“音频”),那么在开释时就可以通过这个信息来确定相应的巨细,而无需额外的处置处罚逻辑。
目前的题目在于,差别类型的资产大概存储方式差别。比方,在文件格式中,数据只是简单地存储在文件中,而别的的信息则是通过额外的盘算得到的。这种方式虽然有一定的灵活性,但在内存开释时会带来额外的复杂性。因此,需要权衡是否继续相沿这种方法,照旧调整存储结构,使内存管理更加规范和同一。
总体而言,需要做的优化包括:
- 确保只能回收“已加载”状态的资产,克制错误地开释正在使用的资产。
- 改进资产槽的结构,存储更多的元数据(如资产类型和巨细),以便开释内存时可以或许正确盘算巨细。
- 同一差别类型资产的内存管理,克制在开释时额外判定资产类型,简化回收逻辑。
- 检查文件格式的存储方式,确保文件数据能有效映射到内存管理结构,减少不必要的盘算和存储开销。
需要进一步思考的是,如安在不增长太多额外开销的情况下,使整个资产管理体系更加高效和易维护。
在 AssetState 中区分位图和声音
为了更高效地管理资产,我们需要在资产槽(slot)中记载资产的类型,比方区分它是位图(bitmap)照旧音频(sound)。目前的体系中,并没有一个直接的方式来存储这个信息,而是在差别的地方进行判定和处置处罚。为了优化这一点,我们可以在资产状态(asset_state)中引入额外的标记位,使其既能表示当前的加载状态,又能区分资产类型。
一种方法是使用状态字段的高位来存储资产类型。比方:
- 设定一个 AssetState_Bitmap 标记,用于标识位图类型资产。
- 设定一个 AssetState_Sound 标记,用于标识音频类型资产。
- 低 8 位用于存储资产的具体状态(如 LOADED、LOCKED 等),高位用于存储类型信息。
这样,在检查资产状态时,可以直接屏蔽掉类型信息,仅关注加载状态。比方,通过 AssetState_Mask 获取资产的基础状态,而高位仍可用于资产类型辨认。这种方式的长处是:
- 同一管理资产类型信息,不需要在差别代码部分进行额外判定。
- 简化加载和卸载逻辑,只需要检查状态字段即可确定资产的类型和当前状态。
- 克制额外的数据结构,减少存储开销,进步访问效率。
在实现过程中,需要修改 uint32 GetState(asset_slot *Slot) 之类的函数,使其可以或许正确剖析状态字段。比方:
- 低 8 位用于表示 LOADED、LOCKED 等状态。
- 高位用于存储 BITMAP 或 SOUND 类型信息。
- 通过位掩码(mask)操作,可以分别获取类型和状态信息。
这样,在资产管理体系中,任何时候查看一个资产槽时,都能立即知道该资产是位图照旧音频,而不需要额外的盘算或存储。这种方法虽然有些“临时拼凑”(janky),但它能有效地完成任务,并使资产管理更加直观和高效。
最终,在卸载资产时,可以通过检查 AssetState_Loaded 确保资产处于可卸载状态,并使用新的类型信息正确地开释内存。这样,整个资产管理流程就更加清楚和同一了。
盘算资产占用的内存量
为了正确开释资产槽(slot)所占用的内存,我们需要盘算该槽实际占用的内存巨细,并开释相应的内存。因此,我们需要创建一个 GetSizeOfAsset 函数,该函数可以或许根据资产的类型(如位图或音频)来盘算其所需的内存量。
具体实现思路
- 添加资产类型掩码(Type Mask)
- 在资产状态字段中,添加一个类型掩码(AssetState_StateMask),用于区分资产是位图(bitmap)照旧音频(sound)。
- 这样,我们可以通过 GetType(asset_slot *Slot) 函数快速获取资产的类型,而不需要额外的存储结构。
- 实现 GetSizeOfAsset 函数
- 该函数接受资产的索引(SlotIndex)和类型Type,并返回该资产实际占用的内存巨细。
- 通过 GetSizeOfAsset 获取资产类型,使用差别的盘算方式盘算巨细:
- 位图(bitmap): 盘算方式为 width × height × 4(假设 4 字节颜色通道)。
- 音频(sound): 盘算方式为 ChennelCount × sample_rate × SampleCount。 sample_rate指sizeof(int16)
- 通过 assert 机制确保只有位图或音频类型的资产被盘算,如果将来扩展了其他资产类型,可以及时发现错误并修正。
- 在开释内存时使用 GetSizeOfAsset
- 在开释资产槽(evict asset)时,调用 GetSizeOfAsset 盘算该资产的巨细,然后调用 ReleaseAssetMemory 开释内存。
- 这样可以确保盘算出的内存巨细与申请时同等,克制重复盘算或差别等的题目。
优化点
- 减少重复盘算:
- 确保 GetSizeOfAsset 盘算出的内存巨细在整个代码中保持同等,不会在多个地方以差别方式盘算同一个资产的巨细,以防止盘算误差或代码冗余。
- 进步可读性:
- 通过 GetSizeOfAsset 获取资产类型,使得代码逻辑清楚,便于维护和扩展。
- 安全性检查:
- 在盘算和开释内存时,通过 assert 机制检查状态,防止错误开释未加载的资产,确保体系稳定性。
最终效果
通过上述改进,我们可以正确地盘算每个资产槽的内存巨细,并在符合的机遇开释它们,从而更高效地管理游戏资产的内存使用,进步体系的稳定性和性能。
消除重复盘算
为了优化内存管理并减少代码重复,我们引入了 asset_memory_size 这一结构,旨在更高效地盘算和存储资产的内存信息。
核心优化点
- 拆分内存盘算逻辑
- 之前,channel size 和 pitch 在多个地方被重复使用,容易导致维护上的题目,比方某个地方修改盘算方式,但另一个地方仍然使用旧逻辑,最终导致 bug。
- 如今,我们将 asset_memory_size 作为一个同一的结构,存储 total size(总巨细)和 section size(行巨细或通道巨细),以便在所有需要的地方复用。
- 新增 asset_memory_size 结构
- 这个结构包罗:
- TotalSize:资产占用的总内存巨细。
- SectionSize:资产的行巨细(bitmap 的 pitch)或通道巨细(sound 的 channel size)。
- 这样,每次调用 GetSizeOfAsset 时,我们不仅可以获得总巨细 TotalSize,还可以获取 SectionSize,从而克制重复盘算。
- 位图(bitmap)和音频(sound)的盘算方式
- 位图盘算
- SectionSize = width × 4 (假设每像素 4 字节)。
- TotalSize = SectionSize × height。
- 音频盘算
- SectionSize = ChennelCount × sizeof(int16)(单个通道的巨细)。
- TotalSize = SectionSize × ChennelCount(所有通道的总巨细)。
- 这样,我们只需盘算 SectionSize,然后直接用于 TotalSize 盘算,减少冗余代码。
- 同一 GetSizeOfAsset 的使用
- 在开释资产(eviction)时,直接调用 GetSizeOfAsset 来获取 TotalSize,用于正确开释内存。
- 在使用资产时,也可以通过 SectionSize 获取 pitch 或 channel size,确保内存结构同等。
具体代码调整
- 界说 asset_memory_size 结构
- struct asset_memory_size {
- uint32_t TotalSize;
- uint32_t SectionSize;
- };
复制代码 - 修改 GetSizeOfAsset
- asset_memory_size GetSizeOfAsset(game_assets *Assets, uint32 Type, uint32 SlotIndex) {
- asset_memory_size Result = {};
- asset *Asset = Assets->Assets + SlotIndex;
- if (Type == AssetState_Sound) {
- hha_sound *Info = &Asset->HHA.Sound;
- Result.Section = Info->SampleCount * sizeof(int16);
- Result.Total = Info->ChennelCount * Result.Section;
- } else {
- Assert(Type == AssetState_Bitmap);
- hha_bitmap *Info = &Asset->HHA.Bitmap;
- uint16 Width = SafeTruncateUInt16(Info->Dim[0]);
- uint16 Height = SafeTruncateUInt16(Info->Dim[1]);
- Result.Section = 4 * Width;
- Result.Total = Height * Result.Section;
- }
- return Result;
- }
复制代码 - 在内存分配和开释时使用
- internal void EvictAsset(game_assets *Assets, uint32 SlotIndex) {
- asset_slot *Slot = Assets->Slots + SlotIndex;
- Assert(GetState(Slot) == AssetState_Loaded);
- asset_memory_size Size = GetSizeOfAsset(Assets, GetType(Slot), SlotIndex);
- ReleaseAssetMemory(Assets, Size.Total, Memory);
- Slot->State = AssetState_Unloaded;
- }
复制代码 附加优化
- 增长 safe_truncate 以安全转换数据类型
- 在 u32 转换为 s16 时,我们没有现成的 safe_truncate,因此新增 safe_truncate_s16 以确保转换安全,防止数据溢出。
- inline int16 SafeTruncateInt16(int32 Value) {
- Assert(Value <= 32767);
- Assert(Value >= -32768);
- int16 Result = (int16)Value;
- return Result;
- }
复制代码
- 这样,在涉及 pitch 或 channel size 盘算时,可以安全地转换,克制溢出题目。
最终效果
- 通过 asset_memory_size 结构,减少重复盘算,进步代码可读性和可维护性。
- 同一 SectionSize 和 TotalSize 盘算,克制差别地方盘算方式差别等的题目。
- 使用 SafeTruncateInt16 保障数据类型转换安全,克制溢堕落误。
- 这样不仅优化了代码结构,还进步了资产管理的稳定性,使内存盘算更加直观可靠。
找到要开释的内存块的位置
我们通过 GetType(Slot) 来确定内存的存放位置,以便更合理地管理和开释内存。此外,为了优化 近来最少使用(LRU, Least Recently Used) 资产的查找,我们引入了一种简单的数据结构来追踪资产的访问顺序。
优化点
1. 通过 GetType(Slot) 同一内存位置判定
- 以前,在开释内存时,我们需要分别判定 sound 和 bitmap 资产的存储方式,代码较为分散且容易堕落。
- 如今,我们用 GetType(Slot) 同一判定:
- internal void EvictAsset(game_assets *Assets, uint32 SlotIndex) {
- asset_slot *Slot = Assets->Slots + SlotIndex;
- Assert(GetState(Slot) == AssetState_Loaded);
- asset_memory_size Size = GetSizeOfAsset(Assets, GetType(Slot), SlotIndex);
- void *Memory = 0;
- if (GetType(Slot) == AssetState_Sound) {
- Memory = Slot->Sound.Samples[0];
- } else {
- Assert(GetType(Slot) == AssetState_Bitmap);
- Memory = Slot->Bitmap.Memory;
- }
- ReleaseAssetMemory(Assets, Size.Total, Memory);
- Slot->State = AssetState_Unloaded;
- }
复制代码 - 这样,代码更加清楚,并且如果将来新增了其他类型的资产(如视频、模子等),只需扩展 GetType(Slot) 的处置处罚逻辑即可。
- 额外添加 assert 断言 以确保将来新增资产类型时不会漏掉相应处置处罚。
2. 计划 LRU 资产回收机制
题目:
- 需要一个机制来找到 近来最少使用(LRU)的资产,以便在内存不足时优先开释。
- 最简单的方法是 双向链表,但它会额外占用内存。
解决方案:
- 使用一个 带头结点(sentinel)的双向链表 来维护所有已加载的资产。
【sentinel】 n. 哨兵, 标记 vt. 警戒, 守卫 [计] 标记 名词复数形式: sentinels; 过去分词: sentinelled;
- 每次 访问 资产时,将其 移动到链表头部,表示近来使用过。
- 需要开释内存时,从 链表尾部 找到最久未使用的资产并开释。
数据结构:
- struct asset_memory_header {
- asset_memory_header *Next;
- asset_memory_header *Prev;
- };
复制代码
使用双向链表跟踪近来最少使用的资产
我们打算实现一个简单的双向链表,来管理和跟踪已加载的资产(比如声音或位图)。每当某个资产被使用时,我们将其移到链表的前端,这样链表末尾的节点就会不停是最久未使用的资产。这种做法非常简单,险些没有复杂的内容。
链表的构建和操作
- 内存盘算:
- 盘算每个资产的内存巨细时,会包罗额外的资产内存头部(header)。这意味着每当我们分配一个资产内存时,就会将这个内存头部附加到数据部分后面。头部的作用是提供关于该资产的信息,这样我们就可以根据内存的位置访问它。
- 内存盘算会分为两部分:一个是数据巨细,另一个是头部的巨细。我们希望在加载数据时知道实际加载的数据巨细,而不是额外的内存头部。
- 内存结构:
- 在加载资产时,我们将内存的巨细与资产头部结合,并盘算出总的内存需求。这使得我们可以明确知道需要加载多少数据。通过修改size字段来实现,这样盘算出的内存巨细就能直接用于加载。
- 比方,我们会把内存位置向前推进,跳过数据部分,得到内存头部的位置。之后,加载的数据就是从内存的起始位置到数据巨细的部分,而头部则位于数据之后的位置。
- 双向链表:
- 双向链表是一种非常方便的数据结构,每个节点不仅有指向下一个节点的指针(next),还包罗指向前一个节点的指针(prev)。这使得我们在遍历或操作链表时可以很方便地从任意位置删除或插入节点。
- 每当一个资产被使用时,它会被移动到链表的前端。链表的末尾则会不停保持为“最久未使用”的资产。
- 克制多线程题目:
- 在处置处罚链表时,克制在多线程情况中进行修改。因为在多线程情况下,修改双向链表结构大概会导致错误,尤其是在节点插入或删除时。
- 因此,我们选择在非多线程情况下进行操作,确保操作的原子性和稳定性。
- 添加资产到链表:
- 每当有新资产被加载进内存时,会天生一个包罗内存地点和资产巨细的资产内存头部。然后,我们将这个头部添加到链表中。
- 这个操作非常直接,只需在加载资产时,把这个资产的内存头部参加链表即可。为了防止出现多线程冲突,所有对链表的操作都会在单线程情况下进行。
实现的简要总结:
- 通过使用双向链表,我们可以或许有效地管理和跟踪已加载的资产,并且每当资产被使用时,我们可以快速地将其移动到链表的前端,使得末尾的资产始终是最久未使用的。
- 接纳内存头部的方式,既可以方便地跟踪资产的内存使用,又可以克制额外的盘算和存储开销。
- 我们选择在单线程情况下进行链表的操作,以克制多线程引发的题目,保证程序的稳定性。
双向链表理论 (黑板)
双向链表(Double Linked List)是一种非常实用的数据结构,它允许在列表中的元素前后进行快速操作。每个节点不仅包罗指向下一个节点的指针(next),还包罗指向上一个节点的指针(prev)。通过这种方式,每个节点都能知道自己前面的节点和后面的节点,提供了比单向链表(Single Linked List)更灵活的操作方式。
双向链表的结构
- 节点(Node):每个节点包罗三个部分:
- 数据部分:存储节点的实际数据。
- 前驱指针(prev):指向前一个节点。
- 后继指针(next):指向下一个节点。
比方,节点的结构可以表示为:
- struct Node {
- Node* prev;
- Node* next;
- Data data;
- };
复制代码
双向链表的长处
- 灵活性:由于每个节点都有指向前一个节点和后一个节点的指针,双向链表比单向链表具有更多的灵活性。比如,在双向链表中,可以方便地从当前节点访问前一个节点,而在单向链表中,只能从当前节点访问下一个节点。
- 删除节点:在双向链表中,删除某个节点非常简单。因为每个节点都能访问到前驱节点和后继节点的指针,所以可以轻松地将前驱节点的next指针指向后继节点,而后继节点的prev指针指向前驱节点,完成节点的删除。相比之下,在单向链表中,删除某个节点时,如果没有指向前驱节点的指针,则无法直接删除。
- 插入和移动节点:双向链表可以在任何位置进行节点插入或删除操作,而不需要遍历整个链表,提供了非常高效的插入和删除操作。
如何操作双向链表
- 删除节点:假设我们有一个要删除的节点,双向链表使得删除变得非常简便。通过访问节点的前驱节点和后继节点,我们可以直接修改它们的指针,跳过要删除的节点:
- prevNode->next = targetNode->next;
- targetNode->next->prev = prevNode;
复制代码 - 插入节点:要在双向链表的某个位置插入节点,首先需要将新节点的前驱指针指向前一个节点,后继指针指向下一个节点,然后更新相邻节点的指针:
- newNode->prev = prevNode;
- newNode->next = prevNode->next;
- prevNode->next->prev = newNode;
- prevNode->next = newNode;
复制代码 - 遍历双向链表:双向链表可以重新到尾遍历,也可以从尾到头遍历,这取决于如何使用next和prev指针:
- 正向遍历:重新开始,依次访问next指针。
- 反向遍历:从尾开始,依次访问prev指针。
与单向链表的区别
- 单向链表(Single Linked List):每个节点只有一个指针,指向下一个节点。删除节点时,如果我们只能访问当前节点,无法直接回到前一个节点,这使得删除操作变得更加困难。
在双向链表中,通过前驱指针,我们可以轻松地删除节点并操作列表。
- 双向链表的“多余性”:双向链表相比单向链表来说,确实提供了更多的操作能力,但也带来了额外的空间开销(每个节点需要两个指针)。因此,只管双向链表提供了更强大的操作灵活性,但它的内存开销也比单向链表大。
总结
双向链表是一个非常灵活且强大的数据结构,特殊适用于需要频繁插入、删除或双向遍历的场景。只管它的内存开销比单向链表要大,但它提供了更高效的操作,可以或许更方便地进行节点的移动、删除和插入。
AddAssetHeaderToList
在实现链表时,接纳了一个名为“哑元”(sentinel)的技术来简化插入操作。这个哑元头部是一个假造的节点,存在于链表的结构中,但并不指向任何实际的资产或数据。通过这种方式,我们能保证链表的头部始终有一个指针可以操作,从而克制了在处置处罚链表时的特殊边界情况。
哑元节点(Sentinel Node)的使用
- 哑元节点的目的:
哑元节点充当链表的起始点,它并不代表任何实际的资产。其唯一作用是作为链表操作的起始点,使得插入和删除操作更为简洁,因为不再需要考虑链表为空或只有一个元素的特殊情况。
- 插入新节点的过程:
- 当需要将一个新的节点(比如新加载的资产)插入到链表时,我们将新节点插入到哑元节点之后,即链表的最前面。
- 哑元节点的 next 指针需要指向新插入的节点。
- 新插入的节点的 previous 指针需要指向哑元节点,而它的 next 指针则指向原本位于哑元节点之后的节点。
- 这样,我们通过更新这些指针,使得新节点顺遂插入链表,并且原本位于该位置的节点的 previous 指针也需要更新,指向新插入的节点。
- 具体步骤:
- 设置哑元节点的 next 指针:将哑元节点的 next 指针指向新插入的节点。
- 设置新节点的 previous 指针:新节点的 previous 指针需要指向哑元节点,这样确保了新节点可以正确回溯到哑元节点。
- 设置新节点的 next 指针:新节点的 next 指针指向原本在哑元节点后面的位置(即哑元节点的 next 指向的节点)。
- 更新原节点的 previous 指针:原本在哑元节点之后的节点的 previous 指针需要更新,指向新插入的节点。
- 插入后的结构:
- 在插入操作完成后,新节点就位于哑元节点之后,成为链表的第一个有效节点。链表的其他节点则继续按照原来的顺序排列。
- 这种方法确保了链表的操作更加简洁,因为每次插入都不需要考虑链表是否为空,也不需要对空链表和只有一个节点的情况进行特殊处置处罚。
通过这种方式,链表的插入和删除操作变得更加同一和简化,因为哑元节点保证了每次操作都能从一个稳定的出发点开始。
指针的语义设置
为了简化节点插入操作,我们通过调整链表中节点的 previous 和 next 指针,使得插入操作更加直观且便于实现。具体来说,在插入节点时,我们首先设置新节点的 previous 和 next 指针,使其指向当前节点前后的相应节点。然后,我们通过调整这些节点的指针,使得链表结构保持同等。
具体操作步骤:
- 设置新节点的指针:
- 新节点的 previous 指针应该指向当前节点的前一个节点(即插入位置的前一个节点)。
- 新节点的 next 指针应该指向当前节点的下一个节点(即插入位置的后一个节点)。
- 调整相邻节点的指针:
- 当前节点前一个节点的指针:当前节点前一个节点的 next 指针应该指向新节点。
- 当前节点后一个节点的指针:当前节点后一个节点的 previous 指针应该指向新节点。
- 插入完成:
- 新节点的 previous 和 next 指针已经被设置好,使得新节点被正确地插入到链表的符合位置。
- 同时,原本位于新节点前后的节点的指针也被更新,确保它们都指向新节点。
总结:
这种方法通过设置新节点的前后指针,并让相邻节点的指针指向新节点,确保链表结构的同等性。这个过程可以通过简单的指针调整来完成,不需要额外复杂的逻辑操作。
RemoveAssetHeaderFromList
在处置处罚双向链表时,移除和插入资产头部(Asset Header)操作非常简便。我们需要做的只是调整相邻节点的指针,确保链表的连接不会中断。
移除资产头部(Remove Asset Header)操作:
- 要移除一个节点(即资产头部),只需调整当前节点的相邻节点的指针即可:
- 前一个节点的 next 指针需要指向当前节点的下一个节点。
- 后一个节点的 previous 指针需要指向当前节点的前一个节点。
- 完成上述步骤后,当前节点就被从链表中移除,链表结构保持完整。
插入资产头部(Add Asset Header)操作:
- 插入时,我们只需要设置新节点的 previous 和 next 指针,使其正确指向新节点前后的节点。
- 新节点的 previous 指针指向当前节点的前一个节点。
- 新节点的 next 指针指向当前节点的下一个节点。
- 然后,调整相邻节点的指针:
- 前一个节点的 next 指针指向新节点。
- 后一个节点的 previous 指针指向新节点。
资产管理流程:
- 在执行资产回收时,首先需要通过 RemoveAssetHeaderFromList 移除资产头部。
- 为了简化操作,资产头部(asset_memory_header)成为了关键的数据结构,用于进行资源管理。
- 通过调用 get least recently used asset 可以获取最不常用的资产,这时只需根据链表的尾部(Sentinel之前的节点)找到最少使用的资产。
改进和优化:
- 在资产添加到链表时,我们可以强制设置资产的 SlotIndex,确保链表中的每个资产都有一个有效的标识。
- 每当资产被访问或使用时,我们需要确保它被移动到链表的前端,标记为“近来使用”,这样可以实现 LRU(近来最少使用)缓存策略。
关于 Sentinel 的使用:
- 为了简化链表操作,使用了一个 “sentinel” 节点,作为链表的基础节点,始终存在于链表中,确保链表至少有一个节点。Sentinel 的 next 和 previous 指针在链表操作时始终指向有效的节点,克制了链表为空的情况。
- 在初始化时,需要确保 sentinel 节点的 next 和 previous 都指向它自己,这样在链表为空的情况下,也能保证操作的正确性。
通过这些方法,整个资产管理流程变得更简洁高效,同时也能保证内存管理和资源回收的灵活性。
初始哨兵设置
在启动时,接纳了一个循环链表的结构,其中的 Sentinel 节点既是头节点,也是尾节点。该 Sentinel 节点的 previous 指针指向它自身,next 指针也指向它自身。这样一来,当插入新的节点时,链表的结构保持同等,不需要额外的检查。
Sentinel 节点的作用:
- 使用一个 Sentinel 节点的利益是,链表总是有一个节点存在,即使链表为空。通过这种方式,链表的操作变得简单,因为我们不需要检查是否为空链表。当我们插入新的节点时,新的节点会将自己的 previous 指针指向 Sentinel,并且它的 next 指针指向 Sentinel 原先指向的节点,这样就保证了链表的完整性。
- 如果没有使用 Sentinel,每次操作时就必须检查链表是否为空,因为链表的 previous 或 next 指针大概为空,这会增长代码复杂性。而使用 Sentinel 之后,链表始终有一个固定的基础节点,克制了这种空指针检查的题目。
内存分配和资产管理:
- 在进行内存分配时,发现了一个小错误,就是在分配内存时没有考虑到总内存的巨细,导致了分配时出现了题目。解决方法是修正为正确的 AcquireAssetMemory 盘算,并且调整了 Size.Total 和 Size.Data,确保内存分配时使用正确的值。
资产回收机制:
- 当前体系中,资产会在内存到达一定限定时被随机逐出。这个机制虽然能确保体系不会超出设定的内存限定,但并没有按照某种特定的顺序进行清除,仅仅是随机逐出一些资产。这样的做法简单,但在某些场景下大概需要更加精确的控制和优化,保证最不常用的资产优先被清除。
总体来看,使用 Sentinel 节点让链表的操作变得更加简单高效,克制了空链表的情况并减少了错误发生的大概性。在内存管理上,也通过随机逐出资产来控制内存的使用量,虽然这是一种简化的做法,但可以或许快速有效地保持体系在内存限定内。
我们应该克制驱逐被锁定的资产
在资产管理中,存在一种“锁定资产”的概念,这类资产是不允许被逐出的,因为它们正在被后台任务使用。为了确保不会在后台任务正在使用时错误地逐出这些资产,我们需要在将资产添加到链表时,检查它是否被锁定。如果是锁定资产,则需要克制将其添加到逐出队列中。
锁定资产的处置处罚:
- 锁定资产是指在某些特殊情况下,比方后台工作线程正在使用这些资产时,这些资产必须保持在内存中,不能被逐出。为了实现这一点,必须确保在资产进入链表时,不会将锁定的资产错误地添加进来。
- 在实现时,筹划中已经考虑到了资产的锁定状态,但目前似乎还没有正确地实现设置资产为“锁定”状态的功能。这是一个需要补充的部分,尤其是在后台任务使用资产时,必须保证这些资产在后台工作过程中不会被开释或逐出。
接下来的步骤:
- 为相识决锁定资产的题目,需要实现一个机制,使得在后台任务使用资产时,可以或许将这些资产标记为锁定状态。只有当后台任务结束后,资产才气被解锁并且有大概被逐出。
- 这个过程将在下一次的开发工作中实现,即将在明天的工作中完成。具体来说,需要添加一个资产锁定的功能,确保后台任务可以或许安全地使用资产,而不会因错误的逐出操作导致崩溃或数据丢失。
总结:
- 在当前的计划中,资产的锁定功能尚未美满,将来将参加锁定机制来防止在后台任务使用期间错误逐出资产。通过对资产的状态进行管理,确保体系的稳定性和内存的合理使用。
双向链表的类型及其实现概述
在双向链表的实现中,存在两种主要的方式:带哨兵节点(Sentinel)和不带哨兵节点(Non-Sentinel)。
不带哨兵节点的链表:
这种方式需要显式地界说链表的头指针(first)和尾指针(last)。在这个链表中,第一个节点的前指针指向空(NULL),而末了一个节点的后指针也指向空(NULL)。此时,链表的第一个节点和末了一个节点需要特殊处置处罚,在插入和删除操作时需要不断检查这些指针是否为空,增长了代码的复杂性。
带哨兵节点的链表:
哨兵节点方法简化了链表的管理。哨兵节点始终存在,并且永远不会被移除。无论链表的长度如何,哨兵节点始终作为链表的出发点和尽头。具体而言:
- 哨兵节点的前指针指向链表的末了一个节点,哨兵节点的后指针指向链表的第一个节点。
- 这样,链表始终保持圆形结构(circular linked list),即末了一个节点的后指针指回哨兵节点,而哨兵节点的前指针指向末了一个节点,形成一个循环。这样,插入和删除操作变得非常简便,因为无需担心链表为空或只有一个元素的特殊情况。
操作简化:
通过使用哨兵节点,链表的第一个节点和末了一个节点不再需要显式存储。它们可以通过访问哨兵节点的**后指针(first)和前指针(last)**来隐式获取。哨兵节点使得每次添加或删除元素时,操作都是同等的,不需要额外的空值检查,因为链表始终有一个完整的节点结构。
- 添加元素:直接插入到哨兵节点四周,哨兵节点的前指针和后指针主动更新,保持链表结构的完整性。
- 删除元素:只需要调整相邻节点的指针,无需特殊处置处罚边界情况。
总结:
使用哨兵节点的双向链表通过简化链表的管理和减少空指针检查,使得链表操作更加简洁高效。通过保持链表的圆形结构,所有操作都可以视作在一个始终存在的结构上进行,克制了额外的判定逻辑,极大地简化了代码的复杂性。
哪个函数拥有指向链表头指针的所有权?
在链表的管理中,通常会有一个题目是“谁拥有链表头指针”的题目。这个题目意味着,需要明确哪个部分的代码或模块负责管理和维护链表的头指针。
链表头指针的所有权
- 拥有链表头指针的模块是指负责管理整个链表的模块或部分。通常,链表的头指针是链表的关键部分,它指向链表的第一个元素(或哨兵节点),并且用于管理链表的操作,如插入、删除、遍历等。
- 这个“拥有”指的是对链表的控制权,比如在需要对链表进行修改(如插入新节点或删除节点)时,这个拥有链表头指针的模块将负责进行相应操作。
管理头指针的责任
- 需要确保头指针始终指向正确的节点。
- 在进行链表操作时(比方,插入或删除节点),必须正确地维护头指针的指向,克制出现指针错误或内存走漏。
- 如果有多个模块需要访问或修改链表,必须确保对头指针的访问是安全的,克制竞争条件或差别等的状态。
总结
“拥有链表头指针”意味着对链表的管理和控制,确保链表结构在操作中始终保持同等和有效。这个责任通常由特定的模块或函数来负担,确保链表的正确操作和内存管理。
如果你关心缓存,链表不是你总是被告知不要使用的吗?
在处置处罚链表时,通常会听到关于缓存友好的建议,尤其是当链表的数据量较大时。如果代码频繁访问链表,大概会遇到缓存未掷中(cache miss)的题目,这会导致性能下降。然而,在不确定代码是否会频繁操作链表时,过早优化缓存并不总是明智的做法。
链表和缓存的关系
- 缓存题目:如果链表的元素分散存储在内存中,访问这些元素时大概会导致缓存未掷中。链表的节点在内存中的分布通常是不一连的,因此每次访问节点时,CPU大概需要从内存中获取数据,这大概会降低性能。
- 优化思路:如果发现链表访问性能成为瓶颈,可以考虑将链表节点会合分配在一块内存区域中,这样可以进步数据的缓存掷中率,从而改善性能。这种方法会将链表节点组织成一个大的一连内存块,而不是单独分散存储。
是否优化链表的缓存性能?
- 在没有明确的性能瓶颈时,过早地考虑链表的缓存友好性并不必要。优化代码时应根据实际情况进行,比方,如果链表代码并未频繁执行,就不需要在这方面过多优化。
- 在代码执行时,最好使用适合当前需求的数据结构。如果链表能满足当前的需求,就可以继续使用。只有在实际发现链表操作导致性能题目时,才需要考虑将链表更换为其他更符合的、更快的数据结构。
总结
链表在某些场景下大概不适合处置处罚大规模、高频率的数据操作,但如果链表是当前最佳的选择,就不需要立即担心缓存题目。首先确保代码的正确性和简洁性,只有在性能成为瓶颈时,才需要考虑优化数据结构。
platform_allocate_memory 函数是否可以分配比哀求的更多的字节,并将巨细存储在那边,以克制需要将其传递给 free 函数?
平台的分配函数通常会分配比哀求的稍多的字节,并将额外的空间用于存储与该内存块相干的数据,以克制将其传递给开释函数。但这种做法并不总是抱负的,因为通常在分配时,已经知道需要的正确巨细,比方在某些情况下,已经明确了内存的需求,所以直接按照所需的巨细进行分配会更加简便。
在这种情况下,采取一种方法是在每个内存块的末尾附加一个列表头,克制了额外的内存管理复杂性。通过这种方式,可以直接受理内存块的巨细和其他元数据,而不需要额外的空间分配和复杂的指针操作。这种做法简化了内存分配过程,使得内存管理更加直接和高效。
在每个资产结构的末尾都有一个列表头,这样做会不会导致缓存大量失效,因为资产结构大概很大?
即使资产结构体大概很大,这种结构不会显著影响缓存,因为缓存是基于较小的内存块(cache line)进行优化的。所以,触及资产结构末尾的链接与触及其他部分的链接没有太大区别。要使这个链表结构更加适应缓存,可以采取的步伐是将链表的链接数据块会合处置处罚。具体来说,当前的结构是“资产数据 -> 链接 -> 资产数据 -> 链接”,如果要进步缓存效率,可以将这些链接数据放在一个单独的缓冲区中,这个缓冲区专门存储所有的链接,像是一个独立的区域存储链接(链接 -> 链接 -> 链接),这样每个缓存行可以包罗多个链接,从而进步缓存的掷中率。
在 RemoveAssetHeaderFromList 中,是否故意义将正在移除的头节点的 prev 和 next 指针清零,照旧这只是多余的清理?这样做有什么利弊?
在从链表中移除节点时,清除被移除节点的前驱指针并不是必须的操作,但为了调试方便,可以做一些额外的检查。比方,可以将被移除节点的 header next 和 header previous 设为零,这样就可以通过调试检查来确认是否出现了题目。这并不会影响性能,因为这个操作的频率通常较低。如果这个操作频繁发生,并且成为性能瓶颈,那么大概需要重新考虑使用链表结构,而选择其他更适合高频操作的数据结构。总的来说,进行这种额外的调试检查是没题目的,但要根据具体情况决定是否进行。
在实际游戏中是否会有一个“头颅喷泉”,大概作为万圣节的物品?
在实际游戏中,大概会有一个“喷泉的头”作为某种物品出现,或许它可以作为万圣节的特殊道具。这听起来是个不错的创意。
完成这个之后,你将如何重新启用实时代码重载功能?
重新启用实时代码重载其实非常简单,即使我们坚持当前的方案。只需让平台代码的循环实时编辑保存一组头文件,并且当进行保存操作时,将这些头文件写入磁盘即可。然而,我甚至建议可以考虑不这样做,而是在进行实时代码编辑时完全使资源缓存失效,这样我们就不需要存储瞬时内存区域。
至于内联函数,它们基本上是一种将函数的代码嵌入调用点的方法,这样可以减少函数调用的开销,尤其是在一些频繁调用的小函数中。通过这种方式,函数调用的指令被直接更换为函数体的代码,从而克制了调用栈和跳转的成本。但需要注意的是,过多使用内联函数大概导致代码膨胀,因为每次调用函数时都会嵌入一份副本。
你能简要讲解一下内联函数吗?
内联函数其实就是给编译器一个提示,告诉它这个函数很大概是小的,应该直接嵌入到调用它的地方,而不是通过传统的函数调用来执行。这样做的目的是希望通过内联优化进步性能,减少调用的开销。编译器收到这个提示后,大概会决定直接把函数的代码插入到调用位置,而不是产生额外的函数调用指令,从而优化代码执行的效率。
然而,需要注意的是,内联函数并不强制要求编译器一定要进行内联,它只是给编译器的一个建议。现代编译器会根据自身的判定来决定是否进行内联,只有在使用了强制内联(如使用__forceinline)时,才会强制要求编译器进行内联。所以,使用inline关键字并不意味着编译器一定会把函数进行内联,它依靠于编译器的优化决策。
此外,过分使用内联函数大概会导致代码膨胀,因为每个调用内联函数的地方都会嵌入该函数的完整代码,这样大概会增长代码的巨细和复杂性。因此,是否使用内联函数需要根据具体情况权衡使用。
你最喜好实现哪种经典数据结构?
在谈到实现数据结构时,最喜好的结构是单链表,因为它非常简单且实现起来很轻松,操作起来也很方便。有些操作,比如添加元素到链表中,甚至可以通过原子互换来完成,这让整个过程变得非常高效和有趣。移除元素时大概需要一些额外的原子互换操作,但总的来说,添加操作的简单性和高效性让单链表成为了一个非常吸引人的选择。
至于体系是否支持热加载的题目,虽然没有具体分析,但通常热加载指的是在运行时动态加载和更新代码或资源,而不需要重新启动体系或应用。如果体系的计划支持这种动态加载机制,那么它可以通过特定的机制来更新资源或功能,而不干扰当前运行的历程或服务。
这个体系是否/将来是否支持资产的热加载?
关于资产的热加载,体系自己并不支持这一功能,因为没有涉及到艺术家的工作,也没有相干的需求。所有的资产文件都是批量提供的,因此并不需要支持热加载。然而,如果有需要,也可以很容易地实现热加载功能。实现方法很简单,比方,可以为加载位图的代码添加功能,检查文件是否存在并从外部加载位图。体系已经有加载位图的代码,如果需要实现这一功能,只需要在加载过程中检查文件路径是否存在,然后从指定位置加载文件。只管目前没有这个需求,但如果需要热加载,实际上黑白常简单且明显的。
编写你自己的非壅闭动态分配器,而不是使用操作体系的内存体系,是否故意义?
讨论了使用自己的非壅闭动态分配器而不是依靠操作体系的内存体系。对于64位体系,操作体系的内存分配器大概富足使用,但在32位体系上使用大概会有些题目,因此有大概会选择实现自己的分配器。本日展示了如何让内存分配工作,但还未涉及内存结构部分。虽然目前没有决定最终的方案,但有很大的大概性会接纳自界说分配器,而不是依靠平台提供的分配器。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |