Godot: 几乎每次都使用最慢的数据结构。

创建于 2018-11-26  ·  61评论  ·  资料来源: godotengine/godot

我正在审查 godot 的代码,我发现完全无视任何类型的 CPU 方面的性能,直到核心数据结构。 当链表是您可以在现代 PC 上使用的最慢的数据结构时,它无处不在地使用 List。 几乎任何其他数据结构都会更快,特别是在小型结构上。 它在可渲染对象中的灯光列表或反射捕获等方面特别令人震惊,您每次都存储 3 个指针,而不仅仅是给它一个包含 8 个最大灯光的堆栈数组(例如)+ 额外的情况比那更多的。

与 RID_Owner 是一棵树相同的事情,它可以是一个哈希映射或一个插槽映射。 用于剔除的八叉树实现也有完全相同的问题。

我想问一下在代码中处处完全和绝对过度使用链表和可怕的性能指针重型数据结构背后的设计意图。 这是出于特定原因吗? 在大多数情况下,一个“分块”链表,在那里你做一个数组的链表会自动为相同的代码带来性能提升。

使用这些数据结构还可以防止大量代码中的任何“简单”并行性,并完全破坏缓存。

我正在努力证明重构一些内部结构以使用更好的数据结构的概念实现。 目前我正在重写剔除代码,它绕过当前八叉树,只使用具有可选并行性的平面数组。 我将使用 tps-demo 作为基准并返回结果,事实上我已经基准测试在那个八叉树上深入 25 个级别......

另一个更令人高兴的是,代码风格的质量给我留下了深刻的印象,一切都很容易理解和理解,而且评论也很好。

discussion enhancement core

最有用的评论

你可能感兴趣。 我已经在花哨的前叉上工作了很长一段时间。 目前的结果是这样的:
在配备 4 核英特尔 CPU 和 GTX 1070 的大型游戏笔记本电脑上拍摄
正常的教父
image

我在https://github.com/vblanco20-1/godot/tree/ECS_Refactor 的叉子
image

在这两个配置文件中,红色本质上是“等待 GPU”。 目前由于 opengl 上的绘图调用太多而遇到瓶颈。 如果没有 vulkan 或重写整个渲染器,就无法真正解决。

在 CPU 方面,我们从 13 ms 帧到 5 ms 帧。 “快速”帧从 5.5 毫秒变为 2.5 毫秒
在“总帧”方面,我们从 13 毫秒到 8-9 毫秒
~75 FPS 到 ~115 FPS

更少的 CPU 时间 = 您可以花更多时间在游戏上。 当前的 godot 在 CPU 上有瓶颈,所以花在游戏上的时间越多意味着 FPS 越低,而我的 fork 是 GPU 限制的,所以你可以添加更多的游戏逻辑,而 FPS 保持不变,因为 CPU 只是“免费”用于相当一段时间每一帧。

很多这些改进都可以合并到 godot 中,如果 godot 支持现代 C++ 并且有一个多线程系统,允许非常便宜的“小”任务。
最大的收益是通过对动态对象进行多线程阴影和多线程光照贴图读取来实现的#25013。 这两者的工作方式相同。 渲染器的许多其他部分也是多线程的。
其他增益是八叉树比 godot one 快 10 到 20 倍,以及一些渲染流程的改进,例如一次记录深度预通过和正常通过渲染列表,而不是迭代剔除列表 2次,以及对 light->mesh 连接方式的重大改变(没有链表!)

我目前正在寻找除 TPS 演示之外的更多测试地图,以便我可以在其他类型的地图中获得更多指标。 我还将撰写一系列文章,详细解释所有这些是如何工作的。

所有61条评论

谁需要性能? :巨魔的脸:

很想知道你测量了什么。

那么这可以解释为什么Godot中一个简单的Light2D节点可以烧毁您的计算机吗?

@Ranoller我不这么认为。 糟糕的照明性能很可能与 Godot 目前执行 2D 照明的方式有关:将每个精灵渲染 n 次(n 是影响它的灯光数量)。

编辑:见#23593

澄清一下,这是关于整个代码中 CPU 方面的低效率。 这与 godot 功能或 godot GPU 渲染本身无关。

@vblanco20-1 有点侧切,你和我曾经讨论过将节点建模为 ECS 实体。 我想知道诀窍是否是用一个新的 ent 模块做一个 godot 的功能分支,该模块将逐渐与树并排工作。 就像 get_tree() 和 get_registry()。 ent 模块可能会遗漏树/场景 80% 的功能,但它可以用作测试平台,特别是对于像组合具有大量对象的大型静态关卡(剔除、流媒体、批量渲染)之类的东西。 减少了功能和灵活性,但提高了性能。

在进行完整的 ECS(我可能会这样做)之前,我想尝试一些低悬的成果作为实验。 以后我可能会尝试完全面向数据。

所以,首先更新:

update_dirty_instances :从 0.2-0.25 毫秒到 0.1 毫秒
octree_cull(主视图):从 0.35 毫秒到 0.1 毫秒

有趣的部分? 八叉树剔除的替代品不使用任何加速结构,它只是迭代一个包含所有 AABB 的哑数组。

更有趣的部分? 新的剔除是 10 行代码。 如果我想让它平行,那将是对线路的一次更改。

我将继续使用灯光实施我的新剔除,看看加速累积了多少。

我们可能应该在主分支上也得到一个谷歌基准目录。 这可能有助于验证,因此人们不必争论它。

godotengine/godot-tests存储库,但还没有多少人使用它。

新更新:

image

我实现了一个相当愚蠢的空间加速结构,有 2 个级别。 我只是每帧生成它进一步的改进可以使它更好并动态更新它而不是重新制作它。

在图像中,不同的行是不同的范围,每个彩色块都是一个任务/代码块。 在那个捕获中,我同时进行八叉树剔除和我的剔除以进行直接比较

它表明重新创建整个加速结构比单个旧八叉树剔除更快。
此外,进一步剔除往往是当前八叉树的一半到三分之一。

除了初始创建的成本之外,我还没有发现我的哑结构比当前八叉树慢的情况。

另一个好处是它对多线程非常友好,并且可以通过并行扩展到您想要的任何核心数量。

它还完全连续地检查内存,因此应该可以轻松地使其执行 SIMD。

另一个重要的细节是它比当前的八叉树简单得多,代码行更少。

你正在引起更多的炒作,而不是权力的游戏结束......

image

稍微修改了算法,现在他们可以使用 C++17 并行算法,这允许程序员只告诉它执行并行 for 或并行排序。
主视锥体剔除中的加速约为 x2,但对于灯光来说,速度与以前大致相同。

对于主视图,我的剔除器现在比 godot 八叉树快 10 倍。

如果您想查看更改,最重要的事情在这里:
https://github.com/vblanco20-1/godot/blob/4ab733200faa20e0dadc9306e7cc93230ebc120a/servers/visual/visual_server_scene.cpp#L387
这是新的剔除功能。 加速结构位于其正上方的函数上。

这应该用VS2015编译吗? 控制台抛出一堆关于 entt\ 中文件的错误

@Ranoller它是 c++17,所以请确保您使用的是较新的编译器

呃,我认为 Godot 还没有转向 c++17 吗? 至少我记得关于这个话题的一些讨论?

Godot 是 C++ 03。这个举动会引起争议。 我建议@vblanco20-1 在胡安结束假期后与他交谈......这个优化会很棒,我们不希望立即出现“这永远不会发生 TM

@vblanco20-1

octree_cull(主视图):从 0.35 毫秒到 0.1 毫秒

有多少对象?

@nem0

@vblanco20-1

octree_cull(主视图):从 0.35 毫秒到 0.1 毫秒

有多少对象?

大约 2200。如果检查非常小,godot 八叉树表现更好,因为它早出。 查询越大,与我的解决方案相比,godot 八叉树就越慢。 如果 godot 八叉树不能提前 90% 的场景,那么它的速度非常慢,因为它本质上是为每个对象迭代链表,而我的系统是数组结构,其中每个缓存行都包含大量 AABB,大大减少缓存未命中。

我的系统有 2 个级别的 AABB,它是一个块数组,每个块可以容纳 128 个实例。

顶层多半是一个AABBs数组+一个Blocks数组。 如果 AABB 检查通过,则我迭代该块。 该块是 AABB 结构数组、掩码和指向实例的指针。 这样一切都是faaaaast。

现在,顶级数据结构的生成是以一种非常愚蠢的方式完成的。 如果它生成得更好,它的性能可能会大得多。 我用 128 以外的不同块大小进行了一些实验。

我检查了不同的数字,每块 128 个似乎仍然是最佳选择。

通过更改算法以将“大”物体与小物体分开,我设法获得了另外 30% 的速度提升,主要是在您没有看地图中心的情况下。 它之所以有效,是因为大对象不会使包含小对象的块 AABB 膨胀。 我相信 3 种尺寸可能效果最好。

块生成仍然不是最佳的。 大块在查看中心时仅剔除大约 10% 到 20% 的实例,而在查看地图外时则高达 50%,因此它执行了大量额外的 AABB 检查,而不是它应该需要的。

我认为改进可能是重用当前的八叉树但“扁平化”它。

现在,即使没有并行执行,我的算法也不会与当前八叉树相同或更差。

@vblanco20-1 我假设您正在与以release模式(使用-O3 )编译的 godot 进行比较,而不是在调试中构建的常规编辑器(没有优化并且是默认值)对?
对不起,如果这是一个愚蠢的问题,但我在线程中没有提到这个问题。
反正干得好:)

@Faless它的 release_debug,确实添加了一些优化。 我无法测试完整的“发布”,因为我无法打开游戏(游戏也需要构建?)

我对如何进一步改进剔除有一个想法,消除重新生成每一帧的需要,并创造更好的空间优化。 我会看看我可以尝试什么。 理论上,这样的东西可以去除再生,稍微提高主剔除的性能,并为点光源提供一种特定的模式,这将大大提高它们的剔除性能,以至于它们的成本几乎为零,因为它将变成廉价的 O(1) 以获取“半径中的对象”。

它的灵感来自我的一个实验是如何工作的,我在做 400.000 个相互反弹的物理对象。

image

现在实施新的剔除。 块生成现在更加智能,导致更快的剔除。 灯的特殊情况尚未实现。
我已经把它提交给了我的叉子

我很好奇您是否可以在当前 master 分支的构建上运行该基准测试工具(屏幕截图中的那个),以便为我们提供一个参考点,了解您的实现比当前实现提供了多少性能。 我是个假人,呜呜。

@LikeLakers2您可以在屏幕截图中看到当前的实现和他的实现。

他在主叉上运行这个

2018 年 12 月 12 日,星期三,MichiRecRoom通知@github.com
写道:

@neikeq https://github.com/neikeq解释一下? 我以为截图
鉴于帖子的方式,已发布的仅是他的实施
到目前为止,屏幕截图已经措辞。


您收到此消息是因为您发表了评论。
直接回复本邮件,在GitHub上查看
https://github.com/godotengine/godot/issues/23998#issuecomment-446831151
或静音线程
https://github.com/notifications/unsubscribe-auth/ADLZ9pRh9Lksse9KBfWV0z5GmHRXf5P2ks5u4crCgaJpZM4YziJp
.

有关于这个话题的消息吗?

有关于这个话题的消息吗?

我和reduz谈过。 永远不会合并,因为它不适合项目。 我转而去做其他实验。 也许我稍后会为 4.0 升级剔除

这是一种耻辱,考虑到它看起来真的很有希望。 希望这些问题能及时得到解决。

也许这应该被赋予 4.0 里程碑? 由于 Reduz 计划在 Vulkan 4.0 上工作,而这个主题虽然是关于性能的,但重点是渲染,或者至少是 OP。

@vblanco20-1 他有没有说它不适合? 我假设是因为转向 c++17?

不过,里程碑还有其他相关问题:#25013

你可能感兴趣。 我已经在花哨的前叉上工作了很长一段时间。 目前的结果是这样的:
在配备 4 核英特尔 CPU 和 GTX 1070 的大型游戏笔记本电脑上拍摄
正常的教父
image

我在https://github.com/vblanco20-1/godot/tree/ECS_Refactor 的叉子
image

在这两个配置文件中,红色本质上是“等待 GPU”。 目前由于 opengl 上的绘图调用太多而遇到瓶颈。 如果没有 vulkan 或重写整个渲染器,就无法真正解决。

在 CPU 方面,我们从 13 ms 帧到 5 ms 帧。 “快速”帧从 5.5 毫秒变为 2.5 毫秒
在“总帧”方面,我们从 13 毫秒到 8-9 毫秒
~75 FPS 到 ~115 FPS

更少的 CPU 时间 = 您可以花更多时间在游戏上。 当前的 godot 在 CPU 上有瓶颈,所以花在游戏上的时间越多意味着 FPS 越低,而我的 fork 是 GPU 限制的,所以你可以添加更多的游戏逻辑,而 FPS 保持不变,因为 CPU 只是“免费”用于相当一段时间每一帧。

很多这些改进都可以合并到 godot 中,如果 godot 支持现代 C++ 并且有一个多线程系统,允许非常便宜的“小”任务。
最大的收益是通过对动态对象进行多线程阴影和多线程光照贴图读取来实现的#25013。 这两者的工作方式相同。 渲染器的许多其他部分也是多线程的。
其他增益是八叉树比 godot one 快 10 到 20 倍,以及一些渲染流程的改进,例如一次记录深度预通过和正常通过渲染列表,而不是迭代剔除列表 2次,以及对 light->mesh 连接方式的重大改变(没有链表!)

我目前正在寻找除 TPS 演示之外的更多测试地图,以便我可以在其他类型的地图中获得更多指标。 我还将撰写一系列文章,详细解释所有这些是如何工作的。

@vblanco20-1 这真是太棒了。 感谢分享!

我也同意,尽管 Vulkan 很酷,但拥有不错的 GLES3 性能对于 Godot 来说绝对是胜利。 完全 Vulkan 支持可能不会很快登陆,而修补 GLES3 是这里的人现在证明完全可行的事情。

我的巨大+1 引起了越来越多的关注。 如果 Reduz 同意允许社区改进 GLES3,我将是地球上最幸福的人。 我们在 Godot 上做一个非常严肃的 3D 项目已经 1.5 年了,我们非常支持 Godot(包括捐款),但是缺乏可靠的 60 FPS 确实让我们失去了动力,并使我们所有的努力都面临巨大的风险。

没有获得足够的学分真的很遗憾。 如果我们当前的(3D 方面非常简单)项目在未来几个月内不能达到 60 FPS,我们将无法提供可玩的游戏,因此所有的努力都将付诸东流。 :/

@vblanco20-1 老实说,我什至在考虑使用你的叉子作为我们游戏的基础。

PS 一些想法:作为最后的手段,我认为甚至完全有可能拥有 2 个 GLES3 光栅化器 - GLES3 和 GLES3 社区。

PS 一些想法:作为最后的手段,我认为甚至完全有可能拥有 2 个 GLES3 光栅化器 - GLES3 和 GLES3 社区。

这对我来说没有意义; 直接在 GLES3 和 GLES2 渲染器中进行修复和性能改进可能是一个更好的主意(只要它们不会在现有项目中以主要方式破坏渲染)。

其他增益是八叉树比 godot one 快 10 到 20 倍,以及一些渲染流程的改进,例如一次记录深度预通过和正常通过渲染列表,而不是迭代剔除列表 2次,以及对 light->mesh 连接方式的重大改变(没有链表!)

@vblanco20-1 是否可以在使用 C++03 的同时优化深度预传递?

@Calinou深度预传递的东西是不需要任何东西并且很容易合并的东西之一。 它的主要缺点是它需要渲染器使用额外的内存。 因为它现在需要 2 个 RenderList 而不是只有一个。 除了额外的内存之外,几乎没有任何缺点。 由于 cpu 缓存,为 1 pass 或 2 构建渲染列表的成本几乎相同,因此它几乎完全消除了 prepass fill_list() 的成本

@vblanco20-1 老实说,我什至在考虑使用你的叉子作为我们游戏的基础。

@and3rson请不要。 这个分叉纯粹是一个研究项目,它甚至不是完全稳定的。 事实上,由于使用了并行 STL,它甚至不会在一些非常特定的编译器之外进行编译。 分叉主要是随机优化思想和实践的试验台。

您使用什么工具来分析 Godot? 您是否需要向 godot 源添加 Profiler.Begin 和 Profiler.End 标志以生成这些示例?

您使用什么工具来分析 Godot? 您是否需要向 godot 源添加 Profiler.Begin 和 Profiler.End 标志以生成这些示例?

它使用 Tracy 探查器,它具有不同类型的范围配置文件标记,我在这里使用。 例如 ZoneScopedNC("Fill List", 0x123abc) 添加具有该名称和所需十六进制颜色的配置文件标记。

@vblanco20-1 - 我们会看到这些性能改进中的一些被带到了 godot 主分支吗?

@vblanco20-1 - 我们会看到这些性能改进中的一些被带到了 godot 主分支吗?

也许当支持 C++11 时。 但是 reduz 在 vulkan 发生之前并不真的想在渲染器上做更多的工作,所以可能永远不会。 优化工作的一些发现可能对 4.0 渲染器有用。

@vblanco20-1 介意分享分支的编译器和环境详细信息,我想我可以解决大部分问题,但如果不需要,请不要浪费时间解决问题。

我们也不能不花太多钱就将 AABB 更改添加到 Godot main 中吗?

@vblanco20-1 介意分享分支的编译器和环境详细信息,我想我可以解决大部分问题,但如果不需要,请不要浪费时间解决问题。

我们也不能不花太多钱就将 AABB 更改添加到 Godot main 中吗?

@swarnimarun Visual Studio 17 或 19(后来的更新之一)工作正常。 (视窗)
修改了 scons 脚本以添加 cpp17 标志。 此外,启动编辑器现在坏了。 我正在努力看看我是否可以恢复它。

可悲的是,AABB 的变化是最大的事情之一。 Godot 八叉树与视觉服务器中的很多东西交织在一起。 它不仅跟踪要剔除的对象,而且还将对象连接到灯光/反射探头,并使用自己的系统将它们配对。 这些对也依赖于八叉树的链表结构,因此几乎不可能在不改变大量视觉服务器的情况下使当前八叉树变得更快。 事实上,对于几种对象类型,我仍然无法摆脱 fork 中的正常 godot 八叉树。

@vblanco20-1 你有 Patreon 吗? 我会为专注于性能的分支投入一些资金,这并不局限于古老的 C++,而且我可能不是唯一一个。 我想在 3D 项目中玩弄 Godot,但我不希望它在没有大量证据的情况下变得可行,一个概念可以向核心团队证明您的性能和可用性方法。

@mixedCase我会非常犹豫是否支持 Godot 的任何类型的分支。 分叉和碎片化往往是开源项目的死亡。

更好的方案,而且现在更有可能证明新的 C++ 允许进行重大优化,Godot 正式升级到更新版本的 C++。 我希望这会在 Godot 4.0 中发生,以最大限度地降低 3.2 中损坏的风险,并及时将新的 C++ 功能与 Vulkan 添加到 Godot 4.0 中一起使用。 我也不希望对 GLES 3 渲染器进行任何重大更改,因为 reduz 想要删除它。

(但我不代表 Godot 开发人员,这只是我的猜测)

@aaronfranke我同意,我认为永久分叉并不理想。 但从我从他所说的(如果我错了请纠正我)中收集到的是, @reduz认为不应使用这些较新的功能,因为它们没有带来任何值得学习这些概念的重要内容,并且因为他认为其中一些使代码“不可读”(至少对于习惯了 C++ 近 10 年前的“增强型 C”的开发人员而言)。

我相信 Juan 是一个非常务实的人,所以可能需要大量的概念证明来证明使用现代抽象和更有效的数据结构的好处,才能让他相信工程上的权衡可能是值得的。 @vblanco20-1 到目前为止已经完成了惊人的工作,所以我个人会毫不犹豫地每个月向他支付一些钱,如果他愿意去做这样的事情。

我永远惊讶于 Godot 的开发者如何表现得好像他们对好的想法、表现和进步过敏。 很遗憾这没有被拉进来。

这是我致力于优化的一系列文章中的一篇,正是关于数据结构使用的文章。

Godot 禁止使用 STL 容器及其 ​​C++03。 这意味着它的容器中没有移动操作符,并且它的容器往往比 STL 容器本身更差。 在这里,我概述了 Godot 数据结构,以及它们的问题。

预分配的 C++ 数组。 在整个发动机周围很常见。 它的大小往往由某些配置选项设置。 这在渲染器中很常见,一个合适的动态数组类在这里会很好用。
误用示例: https :
这个数组无缘无故地浪费内存,在这里使用动态数组将是一个明显更好的选择,因为它只会使它们只有它们需要的大小,而不是编译常量的最大大小。 每个使用 MAX_INSTANCE_CULL 的 Instance* 数组都使用半兆的内存

Vector (vector.h)一个 std::vector 等价的动态数组,但有一个很深的缺陷。
该向量通过写时复制实现原子地引用。 当你做

向量 a = build_vector();
向量 b = a;

refcount 将变为 2。B 和 A 现在指向内存中的相同位置,并且一旦对其进行编辑就会复制该数组。 如果您现在修改矢量 A 或 B,它将触发矢量复制。

每次写入向量时,它都需要检查原子引用计数,从而减慢向量中的每个写入操作。 据估计,godot Vector 至少比 std::vector 慢 5 倍。 另一个问题是当您从中删除项目时,vector 会自动重新定位,从而导致严重的无法控制的性能问题。

池向量(pool_vector.h)。 或多或少与向量相同,但作为池代替。 它具有与 Vector 相同的缺陷,即无缘无故的写入复制和自动缩小。

List (list.h) 一个 std::list 等价物。 该列表有 2 个指针(第一个和最后一个)+一个元素计数变量。 列表中的每个节点都是双向链接的,加上一个指向列表容器本身的额外指针。 列表中每个节点的内存开销为 24 字节。 由于 Vector 通常不是很好,它在 godot 中被过度使用。 在整个引擎上被严重滥用。

严重滥用的例子:
https://github.com/godotengine/godot/blob/master/servers/visual/visual_server_scene.h#L129
没有任何理由将其作为列表,应该是向量
https://github.com/godotengine/godot/blob/master/servers/visual/visual_server_scene.h#L242
这些案例中的每一个都是错误的。 同样,它应该是矢量或类似的。 这实际上会导致性能问题。
最大的错误是在用于剔除的八叉树中使用了 List。 我已经在这个线程中详细介绍了 godot 八叉树。

SelfList(self_list.h)一个侵入式列表,类似于 boost::intrusive_list。 这个总是存储每个节点 32 字节的开销,因为它也指向 self。 在整个引擎上被严重滥用。
例子:
https://github.com/godotengine/godot/blob/master/servers/visual/visual_server_scene.h#L159
这个特别不好。 整个引擎中最糟糕的引擎之一。 这无缘无故地为引擎中的每个可渲染对象添加了 8 个胖指针。 这不需要是一个列表。 同样,它可以是一个槽图或向量,这就是我用它替换的。

Map(map.h)一棵红黑树。 在几乎所有用途中都被误用作哈希图。 应从引擎中删除以支持 OAHashmap 或 Hashmap,具体取决于使用情况。
我看到的每次使用 Map 都是错误的。 我找不到在有意义的地方使用 Map 的示例。

Hashmap ( hash_map.h)大致相当于 std::unordered_map,一个基于链表桶的封闭寻址哈希图。 它可以在删除元素时重新散列。

OAHashMap(oa_hash_map.h)新编写的快速开放

CommandQueueMT(command_queue_mt.h)用于与不同服务器(例如视觉服务器)通信的命令队列。 它的工作原理是将一个硬编码的 250 kb 数组用作分配器池,并将每个命令分配为具有虚拟 call() 和 post() 函数的对象。 它使用互斥锁来保护推送/弹出操作。 锁定互斥体非常昂贵,我建议改用穆迪骆驼队列,这应该快一个数量级。 对于使用视觉服务器执行大量操作(例如大量移动对象)的游戏来说,这可能会成为瓶颈。

这些几乎是 godot 中的核心数据结构集。 没有适当的 std::vector 等价物。 如果你想要“动态数组”数据结构,你就会被 Vector 困住,它的缺点是在写入和缩小规模时的副本。 我认为 DynamicArray 数据结构是 Godot 现在最需要的东西。

对于我的 fork,我使用 STL 和来自外部库的其他容器。 我避免使用 godot 容器,因为它们在性能方面比 STL 差,除了 2 个哈希图。


我在 4.0 中的 vulkan 实现中发现的问题。 我知道它的工作正在进行中,所以还有时间修复它。

在渲染 API 中使用 Map。 https://github.com/godotengine/godot/blob/vulkan/drivers/vulkan/rendering_device_vulkan.h#L350
正如我评论的那样,Map 没有很好的用途,也没有理由存在。 在这些情况下应该只是 hashmap 。
链表过度使用,而不仅仅是数组或类似的。
https://github.com/godotengine/godot/blob/vulkan/drivers/vulkan/rendering_device_vulkan.h#L680
幸运的是,这可能不是在快速循环中,但它仍然是在不应该使用 List 的地方使用它的一个例子

在渲染 API 中使用 PoolVector 和 Vector。 https://github.com/godotengine/godot/blob/vulkan/drivers/vulkan/rendering_device_vulkan.h#L747
没有真正好的理由将这 2 个有缺陷的结构用作渲染 API 抽象的一部分。 通过使用它们,用户被迫使用这 2 个,但有它们的缺点,而不是能够使用任何其他数据结构。 建议在这些情况下使用指针 + 大小,并且如果需要,仍然有一个采用 Vector 的函数版本。

这个 API 会伤害用户的一个实际例子是在顶点缓冲区创建函数上。 在 GLTF 格式中,顶点缓冲区将打包在一个二进制文件中。 使用此 API,用户必须将 GLTF 二进制缓冲区加载到内存中,创建此结构以复制每个缓冲区的数据,然后使用该 API。
如果 API 采用指针 + 大小或 Span<> 结构,用户将能够直接从加载的二进制缓冲区中将顶点数据加载到 API 中,而无需执行转换。

这对于处理程序顶点数据(如体素引擎)的人尤其重要。 使用此 API,开发人员被迫将 Vector 用于他的顶点数据,从而导致显着的性能成本,或者必须将数据复制到 Vector 中,而不是直接从开发人员使用的内部结构加载顶点数据。

如果您有兴趣了解有关此主题的更多信息,我所知道的最好的演讲是来自 CppCon 的演讲。 https://www.youtube.com/watch?v=fHNmRkzxHWs
其他很棒的演讲是这个: https :

Godot 的性能有很大的问题……你会是 godot 团队的一个很好的补充,请考虑一下!

我正在关闭此线程,因为我认为这不是贡献或帮助的正确方式。

@vblanco20-1 再次,我真的很感激您有良好的意图,但您不了解引擎的任何内部工作原理,或其背后的哲学。 您的大部分评论都是针对您并不真正了解它们的使用方式、它们对性能的重要性或它们的总体优先级的。

你的语气也不必要地咄咄逼人。 与其问你不明白的东西,或者为什么以某种方式做某事,你只是傲慢自大。 这不是这个社区的正确态度。

您提到的大部分优化内容只是我计划在 Godot 4.0 中优化的项目数量的一小部分(我的列表中还有更多内容)。 我已经告诉过你,从几个月前开始,这将被重写多次。 如果你不相信我,那也没关系,但我觉得你在浪费自己的时间,并且无缘无故地混淆了很多人。

一旦我完成了我的工作,我显然非常欢迎你的反馈,但你现在所做的只是打败一匹死马,所以冷静一会儿。

同样,当 Godot 4.0 的 alpha 推出时(希望在今年年底之前),欢迎您分析所有新代码并就如何进一步优化它提供反馈。 我相信我们都会受益。 就目前而言,在这里讨论其他任何内容没有多大意义,因为现有代码将在 4.0 中消失,并且在 3.x 分支中不会合并任何内容,此时稳定性比优化更重要。

由于许多人可能对技术细节感到好奇:

  • 所有空间索引代码(vblanco 重写的内容)将被用于多线程剔除的线性算法取代,结合用于分层遮挡剔除的八叉树和用于重叠检查的 SAP,这很可能是最好的全能算法,保证良好在任何类型的游戏中的表现。 这些结构的分配将与新的 RID_Allocator 相同,即 O(1) )。 我之前与@vblanco20-1 讨论过这个问题,并解释说他的方法不能很好地适用于所有类型的游戏,因为它需要用户具有一定程度的专业知识来进行调整,而典型的 Godot 用户通常不希望拥有这些专业知识。 这也不是添加遮挡剔除的好方法。
  • 当可以使用列表时,我不会使用数组,因为列表会在零碎片化风险的情况下进行小的时间分配。 在某些情况下,我更喜欢分配一个始终增长且永不收缩的分段数组(与页面对齐,因此它们会导致 0 个碎片)(例如 RID_Allocator 或 2D 引擎 Vulkan 分支中的新 CanvasItem,现在允许您重绘项目很多命令非常有效),但必须有一个性能原因。 当在 Godot 中使用列表时,是因为小分配比性能更受欢迎(实际上它们使代码意图更清晰以供其他人阅读)。
  • PoolVector 适用于具有连续内存页的非常大的分配。 在 Godot 2.1 之前,它使用预分配的内存池,但在 3.x 中已删除,现在当前的行为是错误的。 对于 4.0,它将被替换为虚拟内存,这是要做的事情。
  • 将 Godot 的 Vector<> 与 std::vector 进行比较是没有意义的,因为它们有不同的用例。 我们主要使用它来传递数据并锁定它以便快速访问(通过 ptr() 或 ptrw() 方法)。 如果我们使用 std::vector,由于不必要的复制,Godot 会慢得多。 我们还利用了许多不同用途的写入机制的副本。
  • Map<> 对于大量元素来说更简单和友好,无需担心快速增长/收缩造成碎片。 当需要性能时,使用 HashMap 代替(虽然这是真的,应该多使用 OAHashMap,但它太新了,从来没有时间这样做)。 作为一般的哲学,当性能不是优先考虑的时候,小分配总是比大分配更受欢迎,因为操作系统的内存分配器更容易找到小孔来放置它们(这几乎是它的设计目的) ,有效地消耗更少的堆。

同样,您可以随时询问计划和设计,而不是流氓和抱怨,这不是帮助项目的最佳方式。

我也确定许多阅读此线程的人都想知道为什么空间索引器开始时很慢。 原因是,也许您是 Godot 的新手,但直到最近,3D 引擎已有 10 多年的历史并且非常过时。 已在 OpenGL ES 3.0 中对其进行了现代化改造,但由于我们在 OpenGL 中发现的问题以及它在 Vulkan 中被弃用(Apple 放弃了它)的事实,我们不得不停止。

除此之外,Godot 不久前还运行在像 PSP 这样的设备上(它只有 24mb 的内存可用于引擎和游戏,所以很多核心代码在内存分配方面非常保守)。 由于硬件现在非常不同,这正在被更改为更优化并使用更多内存的代码,但是当不需要时,执行此工作毫无意义,并且您会看到在许多地方使用列表性能确实很好没关系。

此外,我们想做的许多优化(将许多互斥体代码移动到原子以获得更好的性能)必须暂停,直到我们可以将 Godot 移动到 C++11(它对内联原子有更好的支持,并且它没有要求您包含 Windows 头文件,这会污染整个命名空间),这在稳定分支中是无法做到的。 迁移到 C++11 将在 Godot 3.2 分支和功能冻结之后进行,否则保持 Vulkan 与 Master 分支同步将是一个巨大的痛苦。 不用着急,因为目前的重点是 Vulkan 本身。

抱歉,事情需要时间,但我更喜欢正确完成而不是匆忙完成。 从长远来看,它的回报更好。 现在所有的性能优化都在进行中,应该很快就准备好了(如果你测试了 Vulkan 分支,2D 引擎比以前快得多)。

嗨,瑞杜兹,
虽然我大多认为你的观点是有效的,但我想评论两个我不同意的观点:

  • 当可以使用列表时,我不会使用数组,因为列表会进行零碎片化风险的小时间分配。 在某些情况下,我更喜欢分配一个始终增长且永不收缩的分段数组(与页面对齐,因此它们会导致 0 个碎片)(例如 RID_Allocator 或 2D 引擎 Vulkan 分支中的新 CanvasItem,现在允许您重绘项目很多命令非常有效),但必须有一个性能原因。 当在 Godot 中使用列表时,是因为小分配比性能更受欢迎(实际上它们使代码意图更清晰以供其他人阅读)。

我非常怀疑链表是否会比指数增长的动态数组具有更好的整体性能,无论是速度还是内存效率。 后者保证最多占用实际使用空间的两倍,而List<some pointer>使用的存储空间正好是其三倍(实际内容、next 和 prev 指针)。 对于分段数组,情况看起来更好。

当正确包装时(我可以从我已经看到的 godot 代码中看出,它们是),它们对程序员来说看起来几乎一样,所以我不明白你的意思是“它们 [列表] 使代码意图更加清晰”。

恕我直言,列表在两种情况下都有效:

  • 您需要经常擦除/插入容器中间的元素
  • 或者您需要固定时间(而不仅仅是分摊固定时间)插入/删除元素。 即,在不可能进行耗时的内存分配的实时环境中,它们很好。
  • Map<> 对于大量元素来说更简单和友好,无需担心快速增长/收缩造成碎片。 当需要性能时,使用 HashMap 代替(虽然这是真的,应该多使用 OAHashMap,但它太新了,从来没有时间这样做)。 作为一般的哲学,当性能不是优先考虑的时候,小分配总是比大分配更受欢迎,因为操作系统的内存分配器更容易找到小孔来放置它们(这几乎是它的设计目的) ,有效地消耗更少的堆。

libc 分配器通常非常聪明,并且由于 OAHashMap(或 std::unordered_map)不时重新分配它们的存储(摊销常量时间),分配器通常设法保持其内存块紧凑。 我坚信,一个OAHashMap不能有效地消耗比一个普通二叉树地图像地图更多的堆。 相反,我非常确定 Map 的每个元素中巨大的指针开销实际上比 OAHashmap(或 std::unordered_map)的任何堆碎片消耗更多的内存。

毕竟,我认为理清这些争论的最好方法是对其进行基准测试。 当然,这对 Godot 4.0 有更大的用处,因为 - 正如你所说 - 那里会发生很多性能优化,并且没有多少改进代码路径的用处,无论如何,这些代码路径很可能在 4.0 中完全重写。

但是@reduz ,您如何对 @vblanco20-1 建议的所有这些更改进行基准测试(甚至可能是现在,在 3.1 中)。 如果@vblanco20-1(或其他任何人)愿意花时间编写这样一个基准测试套件并根据 vblanco 的变化来评估 Godot3.1 的性能(在速度和“考虑碎片的堆消耗”方面)? 它可能会为实际的 4.0 更改提供有价值的提示。

我认为这样的方法很适合您的“[事情] 做得很好,而不是匆忙完成”。

@vblanco20-1:事实上,我很欣赏你的工作。 您是否有动力创建此类基准,以便我们实际衡量您的更改是否是实际的性能改进? 我会很感兴趣。

@Windfisch我建议您重新阅读我上面的帖子,因为您误读或误解了一些事情。 我会为你澄清它们。

  • 列表完全用于您描述的用例,我从未声称它们具有更好的性能。 它们比数组更有效地重用堆,因为它们由小分配组成。 在规模上(当你在他们的预期用例中大量使用它们时),这真的很重要。 当需要性能时,其他更快、更紧凑或具有更好缓存一致性的容器已经被使用。 无论好坏,Victor 主要专注于较旧的引擎领域之一(如果不是实际上最古老的),该领域从未优化过,因为它是用于为 PSP 发布游戏的内部引擎。 很长一段时间以来,这都需要重写,但还有其他优先事项。 他的主要优化是我最近添加的基于 CPU 的体素锥体追踪,老实说,我在这方面做得不好,因为它在 3.0 版本附近太匆忙了,但对此的正确修复是完全不同的算法,而不是像他一样添加并行处理。
  • 我从来没有争论过@vblanco20-1 的工作表现,坦率地说,我并不关心它(所以你不需要让他浪费时间做基准测试)。 不合并他的工作的原因是因为 1) 他使用的算法需要根据游戏中对象的平均大小手动调整,这是大多数 Godot 用户需要做的事情。 我倾向于支持可能稍微慢一点但可扩展性更好的算法,而无需进行调整。 2)他使用的算法不利于遮挡剔除(简单八叉树由于分层性质更好)。 3)他使用的算法不利于配对(SAP往往更好)。 4) 他使用 C++17 和我不感兴趣支持的库,或者我认为不必要的 lambdas 5) 我已经在为 4.0 优化它,并且 3.x 分支具有稳定性作为优先事项,我们打算尽快发布 3.2,所以这不会被修改或在那里工作。 现有代码可能较慢,但它非常稳定且经过测试。 如果这被合并,就会有错误报告、回归等。没有人有时间处理它或帮助 Victor,因为我们已经忙于 4.0 分支。 上面已经解释过了,所以我建议你重新阅读这篇文章。

无论如何,我向 Victor 承诺,索引代码可以是可插入的,因此最终也可以为不同类型的游戏实现不同的算法。

Godot 是开源的,他的 fork 也是开源的。 我们在这里都是开放和分享的,如果你需要的话,没有什么可以阻止你使用他的作品。

由于优化似乎不会影响 gdscript、渲染或“口吃”问题,而且有些事情人们会抱怨(我包括),也许随着优化人们会很高兴(我包括)......不需要lua jit 速度...
在“写时复制”中的工作是我的插件中非常大的性能优化(从脚本解析中的 25 秒到 7000 行脚本中的仅 1 秒)......我觉得这种优化是我们需要,在 gdscript 中,在渲染和解决口吃问题......就是这样。

感谢@reduz的澄清。 它确实使您的观点比以前的帖子更清楚。

空间索引代码是可插入的,这很好,因为在处理大量不同比例的对象时,确实有人会摔倒在我的脚上。 期待4.0。

我也在考虑这个问题,我认为将一些包含优化空间索引的想法的共享文档放在一起可能是个好主意,这样更多的贡献者可以了解这一点,并提出想法或执行。 我对需要做的事情有很好的想法,但我相信这里有很多空间可以做进一步的优化并提出有趣的算法。

我们可以提出非常明确的要求,我们知道算法必须满足这些要求(例如,如果可能的话,不要求用户对世界大小的平均元素进行调整,如果可能的话,不要使用线程进行暴力破解——它们不是免费的,其他部分引擎也可能需要它们,例如物理或动画,并且它们在移动设备上消耗更多电池-与基于重新投影的遮挡剔除兼容-因此,可能需要某种形式的层次结构,但也应针对蛮力进行测试-,要聪明关于阴影缓冲区更新 - 如果没有任何变化就不要更新 - 探索优化,例如基于重新投影的遮挡剔除等方向阴影)。 我们还可以讨论一些基准测试的创建(我认为 TPS 演示不是一个好的基准测试,因为它没有那么多对象或遮挡)。 @vblanco20-1 如果您愿意遵循我们的编码/语言风格和理念,当然非常欢迎您伸出援手。

其余的渲染代码(使用 RenderingDevice 的实际渲染)或多或少是直接的,没有很多方法可以做到,但索引似乎是一个更有趣的优化问题需要解决。

@reduz以供参考空间索引。 原来的基于 tile 的剔除被移除,取而代之的是一个八叉树,大约在这个线程的一半。 我得到的八叉树是 WIP(缺少一些重新拟合的功能),但结果对于原型来说非常好。 从它的原型性质来看,它的代码并不是那么好,所以它唯一有用的是检查这种八叉树在复杂场景(如 tps-demo)中的表现。
它的灵感来自于虚幻引擎八叉树的工作原理,但进行了一些修改,例如平面迭代的可能性。

主要思想是只有八叉树上的叶子持有对象,而这些对象保存在大小为 64(编译时大小,可以不同)的数组中。 八叉树的叶子只有在“溢出”为 65 个元素时才会分裂。 当你移除对象时,如果父节点的每个叶子都适合 64 大小的数组,那么我们将叶子合并回它们的父节点,它成为一个叶子。

通过这样做,我们可以最大限度地减少节点上的测试时间,因为八叉树最终不会太深。

我做的另一件好事是叶节点也存储在一个平面数组中,这允许在剔除中进行并行处理。 这样,在对点阴影或其他“小”操作进行剔除时可以使用分层剔除,而对于主视图可以使用平面平行剔除。 当然可以只对所有内容使用分层,但可能会更慢并且无法并行化。

块结构会浪费一点内存,但即使在最坏的情况下,我也不认为它会浪费大量内存,因为如果节点低于一定数量,节点就会合并。 它还允许使用池分配器,因为节点和叶子都将具有恒定大小。

我的 fork 中也有多个八叉树,用于优化一些东西。 例如,我有一个仅用于 shadowcaster 对象的八叉树,它允许在剔除阴影贴图时跳过与“可以投射阴影”相关的所有逻辑。


关于我对 Vector 和其他渲染 API 的担忧,这个问题解释了我所担心的。 https://github.com/godotengine/godot/issues/24731


在 fork 的库和 C++17 的东西上......那是不必要的。 fork 过度使用了很多库,因为我需要它们的某些部分。 唯一真正需要的,而且我认为 Godot 需要的是一个行业强度的并行队列,我使用了穆迪骆驼队列。


在 lambda 上,它们的用法主要用于剔除,它用于节省大量内存,因为您只需要将通过检查的对象保存到输出数组中。 另一种方法是做迭代器对象(这就是虚幻引擎对它们的八叉树所做的),但它最终会成为更糟糕的代码并且更难编写。


我的意图不是“流氓”,而是回答关于“你可以自由制作叉子来演示”的频繁评论,这正是我所做的。 线程上的第一条消息有点危言耸听,不太正确。 对不起,如果我对你显得粗鲁,因为我唯一的目标是帮助最流行的开源游戏引擎。

@vblanco20-1 听起来不错,你提到八叉树的方式很有道理。 老实说,我不介意实现并行队列(看起来足够简单,不需要外部依赖,并且需要 C++11 才能正确执行,所以猜测这只会发生在 Vulkan 分支上)。 如果你提交了这个,我肯定想看看它,所以我可以用它作为 Vulkan 分支中索引器重写的基础(我只是重写那里的大部分内容,所以无论如何都需要重写)。 当然,如果你想帮助实现(可能当新 API 有更多东西时),这是非常受欢迎的。

使用具有扁平数组的并行版本很有趣,但我的观点是它对遮挡剔除没有那么有用,并且测量与常规八叉树剔除相比有多少改进会很有趣,考虑到使用的额外内核数量。 请记住,还有许多其他领域可能会使用多线程,从而可以更有效地(减少蛮力)使用它(例如物理学),因此即使使用这种方法我们获得了相对微不足道的改进,也并非总是如此出于最佳利益,因为它可能会导致其他区域无法使用 CPU。 也许它可以是可选的。

我还发现 Bullet 3 对 dbvt 的实现非常有趣,它进行增量自平衡和线性分配,这是我想研究更多的事情之一(我已经看到这种方法在我无法提及的专有引擎中实现:P),就算法而言,平衡二叉树在设计上比八叉树冗余少得多,并且可能在配对和遮挡测试中都可以更好地工作,因此可插入的宽相/剔除方法实际上可能是一个好主意,因为我们使适当的有用基准场景。

在任何情况下,你都非常聪明,如果我们能在这方面合作就太好了,但我只想请你理解并尊重我们现有的许多设计理念,即使它们可能并不总是适合你的口味. 工程浩大,惯性力很大,为了品味而改变事物并不是一个好主意。 在设计方面,用户友好性和“正常工作”始终排在第一位,代码的简单性和可读性通常优先于效率(即,当效率不是目标时,因为某个区域并不重要)。 上次我们讨论的时候,你想改变整个事情,不想听任何推理或关注实际问题,这不是我们通常与社区和贡献者合作的方式,这就是为什么我的建议是冷静好意。

谁需要性能? :巨魔的脸:

性能和源模型使我与 Godot 保持一致。

编辑:对不起,也许我有些题外话,但我想澄清开源的好处。

@mixedCase我会非常犹豫是否支持 Godot 的任何类型的分支。 分叉和碎片化往往是开源项目的死亡。

我不这么认为。 开源的本质就是你可以随心所欲地使用和重用代码。 所以这不会是某个项目的结束,而是为用户提供了更多的选择。
你说的是垄断,那不是捍卫开源的。 不要被那些假装向您表明拥有完全控制权更好的公司所欺骗。 这是一个谎言,至少在开源世界中是这样,因为如果其他人拥有代码,那么您也拥有代码。 他们需要做的是照顾如何处理社区或如何处理社区。

无论如何,原始开发人员可以从分叉中合并改进。 他们总是可以自由地这样做。 这是开源世界的另一种性质。

在最坏的情况下,分叉会比原来的更好,很多贡献者会去那里。 没有人输,都是赢。 哦,对不起,如果一家公司落后于原始公司,他们可能会失败(或者他们也可以合并分叉的改进)。

此页面是否有帮助?
0 / 5 - 0 等级