需要非常好的性能的大项目真的不用多态吗?

很长一段时间以来,我一直对 C++ 的性能感兴趣。

很多事情不断出现,无论是在会议中还是在书籍中:

不要使用虚函数,在缓存、分支等中有数据。

有许多带有视频游戏示例的基准测试来显示性能差异。 问题是,示例总是非常简单。

这在超过 20 行的代码中是如何工作的?在 AAA 视频游戏、金融等领域

如果我有 100 种具有不同更新、不同行为和其他乐趣的对象,那么通过多态或函数指针很容易设置。

现在,按照给出的建议制作强大的代码,上述选项是不可能的。 因此,我们更愿意拥有 100 个单独更新的阵列。 我们将可以很好地访问缓存,函数可能会内联等,简而言之,性能原则上会更好。

因此,我必须在每一帧调用 100 个数组和 100 个不同的函数。 表格会根据发生的情况、新玩家的出现、怪物的死亡等动态变化。 一些数组将有 0 个元素处于活动状态,其他 10 ... 我会调用不起作用的函数(没有活动元素的数组),但我别无选择,我必须有一个标志或查看数组中元素是否处于活动状态。

我最终得到了这样的结果:

obj1update ();
obje2update ();
....

obj1behavior ();
obj2behavior ();
....

obj1render ();
obj2render ();
.....

objectxy ();
....

当然,毫无疑问会有一个函数来管理对象上的所有更新调用,一个用于行为等,但为了简化它,如上所示。

为了留在电子游戏中,如果玩家的 x 动作导致 y 个怪物的到来,那么有几种类型的怪物具有不同的功能。 因此,我们将把怪物放在我们的表中,这次处理这些怪物的函数将起作用。

我们可以使用 ECS 模式或衍生模式,但它们可能具有非常不同的行为(指导它们或其他的 ia)、不同的组件,因此需要不同的函数来处理它们。 由于我们没有多态性或函数指针,因此在代码中将很难调用它们,而且我们必须在每一帧检查它们是否有要处理的东西。

真的是这样吗?假设我有 500 种类型? 1000 ?

编辑:

很多评论,所以我会在这里回复你。

正如 Federico 所说,我想知道这些建议是否对书籍有益,但在实践中不太适用。

我看过的几个资源:

https://www.agner.org/optimize/#testp 很棒的几本书套装

www.youtube.com/watch?v=WDIkqP4JbkE&t Scott Meyers 谈记忆

https://people.freebsd.org/~lstewart/articles/cpumemory.pdf 在内存中

www.youtube.com/watch?v=rX0ItVEVjHc&t 面向数据的编程

https://www.dataorienteddesign.com/dodbook/ 面向数据的设计书籍

还有其他资源,但它已经让您了解我所基于的内容

Robert1973 回答:需要非常好的性能的大项目真的不用多态吗?

不,真正的程序不是那样写的。真正的程序是通过注意到所有的怪物有一堆共同点,并使用相同的代码来完成这些事情而编写的。他们都是寻路者,但他们可以走的距离不同?伟大的。添加一个 max_walking_distance 变量并每次调用相同的函数。

你所有的怪物都有 3D 模型吗?那么您就不需要虚拟 render 方法。您可以只渲染模型。

您不必根据“合理”边界划分数据。您不必必须struct monster。您可以有一个 struct monster_pathfinding 和一个 struct monster_position 以及一个 struct monster_3d_model。即使你只是将它们放在并行数组中(即怪物 123 的寻路信息在 monsters_pathfinding[123] 中,它的位置在 monster_positions[123] 中),这也可以更有效地利用数据缓存,因为寻路代码没有t 将 3D 模型指针加载到缓存中。如果某些怪物寻路或渲染,您可以通过跳过条目来变得更聪明。从本质上讲,为了提高性能,建议您根据数据的使用方式将数据组合在一起,而不是根据您对游戏中事物的心理模型。是的,跳过条目会使删除怪物变得更加困难。但是你刷怪物的次数很多,你也不会经常删除怪物,对吧?

也许只有少数怪物向玩家开枪(其余的怪物试图吃掉玩家)。你可以有一个 struct monster_gun_data {int ammunition; int max_ammunition; int reload_time; monster_position *position;};,然后如果你有 200 个怪物,但其中只有 10 个有枪,你的 monstersShootGunsAtPlayers 函数只需要遍历 monster_gun_data 数组中的 10 个条目(和通过指针加载它们的位置)。或者,您可能会对其进行分析并发现因为大多数 游戏中的怪物都有枪,所以遍历所有怪物并检查它们的 MONSTER_HAS_GUN标志,而不是通过不能轻易预取的指针访问位置。

你如何进行不同种类的怪物攻击?好吧,如果它们完全不同(近战与远程),那么您可能会按照您所描述的使用不同的功能来完成它们。或者你可能只在你决定怪物想要攻击玩家后检查攻击类型。你似乎建议怪物使用不同的攻击代码,但我敢打赌这几乎适用于所有怪物:

if(wantsToAttack(monster,player)) {
    if((monster->flags & HAS_RANGED_ATTACK) && distance(monster,player) > monster->melee_attack_distance)
        startRangedAttack(monster,player);
    else
        startMeleeAttack(monster,player);
}

持枪的怪物和持弓箭的怪物真正有什么不同?攻击速度、动画、射弹的移动速度、射弹的 3D 模型以及它造成的伤害量。这就是所有数据。那不是不同的代码。

最后,如果您有完全不同的东西,您可以考虑将其设为具有虚函数的“策略对象”。或者只是一个普通的函数指针,如果可以的话。请注意,Monster 对象仍然不是多态的,因为如果是多态的,我们就不能拥有它们的数组,这会减慢所有公共代码的速度。只有我们所说的多态怪物的特定部分实际上是多态的。

void SpecialBossTickFunction(Monster *monster) {
    // special movement,etc
}
// ...
monster->onTick = &SpecialBossTickFunction;
// monster is still not polymorphic except for this one field

你也可以这样做:

struct SpecialBossTickStrategy : TickStrategy {
    void onTick(Monster *monster) override {...}
    // then you can also have extra fields if needed
    // but you also have more indirection
};
monster->onTick = new SpecialBossTickStrategy;

不要做不必要的事情。尝试使用事件驱动,而不是每刻都做某事:

// bad because we're calling this function unnecessarily every tick
void SpecialUndeadMonsterTickFunction(Monster *monster) {
    if(monster->isDead) {
        // do some special reanimation sequence
    }
}
monster->onTick = &SpecialUndeadMonsterTickFunction;

// better (for performance)
void SpecialUndeadMonsterTickWhileDeadFunction(Monster *monster) {
    // do some special reanimation sequence
    if (finished doing whatever) {
        monster->onTick = NULL;
    }
}
void SpecialUndeadMonsterDeathFunction(Monster *monster) {
    monster->onTick = &SpecialUndeadMonsterTickWhileDeadFunction;
}
// ...
monster->onDead = &SpecialUndeadMonsterDeathFunction;

// Also better (for performance)
void DoUndeadMonsterReanimationSequences() { // not virtual at all,called from the main loop
    for(Monster *monster : special_undead_monsters_which_are_currently_dead) {
        // do some special reanimation sequence
    }
}

// Not great,but perhaps still less bad than the first one!
void DoUndeadMonsterReanimationSequences() { // not virtual at all,called from the main loop
    for(Monster &monster : all_monsters) {
        if(monster.type == TYPE_SPECIAL_UNDEAD_MONSTER && monster.isDead) {
            // do some special reanimation sequence
        }
    }
}

请注意,在第三个示例中,您必须使此数组 special_undead_monsters_which_are_currently_dead 保持最新。没关系,因为您只需要在怪物生成、消失、死亡或未死亡时更改它。这些都是相对罕见的事件。您正在这些事件中做更多的工作,以节省每个滴答声。


最后,请记住这些技术可能会或可能不会提高您实际程序的性能。我认为国防部是一个想法的抓包。 它并不是说你必须以某种方式编写你的程序,但它提供了一堆非常规的建议、解释它们为什么工作的理论,以及其他人如何设法做到的例子在他们的程序中使用它们。由于 DOD 通常建议您完成对数据结构的重组,因此您可能只想在程序的性能关键领域实施它。

,

只是为顶级问题添加更多视角:

要求非常好的性能的大项目真的不用多态吗?

您错过了一整类多态性。

我经常在一个项目中混合使用以下所有三种样式,因为并非所有代码都具有相同的性能要求:

  1. 设置和配置代码通常不需要(非常)快。对属性、工厂等使用 OO 风格和运行时多态性。

    运行时多态泛指虚函数。

  2. 确实需要快速的稳态代码通常可以使用编译时多态性。这适用于具有相似接口的静态已知(理想情况下为小型)类型集合。

    编译时多态意味着模板(函数模板、类型模板、用等效的策略替换运行时策略模式等)

  3. 具有最严格性能要求的代码可能需要面向数据(即围绕缓存友好性而设计)。

    这不是项目中的全部代码,甚至可能不是所有需要快速处理的代码。这是所有需要快速且性能受缓存效果支配的代码。

    如果你只有一个对象的副本,你可能会尽可能地内联(并尝试将它放入尽可能少的缓存行中),但是将它分成四个不同的数组,每个数组只有一个元素不会取得很大成就。

本文链接:https://www.f2er.com/23527.html

大家都在问