【问题标题】:Combining subclasses which have different fields for efficiency?结合具有不同领域的子类以提高效率?
【发布时间】:2010-02-07 22:50:48
【问题描述】:

我正在开发一款 Java Android 游戏。这里的游戏一般需要使用内存池来避免垃圾回收。

为了论证,假设我有以下类:

// Enemy in the game
class Enemy { int x; int y; abstract void update(); }
// Enemy that just bounces around
class Roamer extends Enemy { int direction; ... }
// Enemy that chases a player
class Chaser extends Enemy { Player target; ... }
// maybe 20 more enemy types...

根据需要使用内存池时,使用子类是一件痛苦的事情,例如说明您需要多少 Roamer 和 Chaser 对象,而不仅仅是您可能需要多少 Enemy 对象。此外,更新的虚函数调用可能很慢。

我想到的另一种选择是将所有这些类合并为一个,例如

class Enemy
{
    int direction;
    Player target;
    int type; // i.e. ROAMER_TYPE, CHASER_TYPE
}

然后我会有一个更新函数来检查“类型”变量并相应地更新。这显然是一种更像 C 的方法。不过感觉很hacky,因为例如漫游敌人将有一个它从不使用的“目标”变量。不过,我一次不太可能在内存中拥有超过 100 个敌人,所以这真的不是什么大问题。我只想在漂亮的代码和速度之间做出妥协。

有没有人知道如何最好地构建这个?像这样合并类是不是太过分了?这是个好主意吗?我应该只使用常规的类树吗?

我知道这个问题没有正确答案,但我想要一些不同的观点。

【问题讨论】:

  • 如果您采用第二种方法,请务必使用枚举而不是整数,以确保类型安全。
  • 在 android developer.android.com/guide/practices/design/… 下严格不推荐枚举。我真的不喜欢这种方法,因为这意味着我必须输入两次文件名(一次在引号中,一次用于常量)。

标签: java android oop


【解决方案1】:

在这里组合会比继承更好吗?例如,像这样:

class Enemy {
    int direction;
    Player target;
    Strategy updater;
}
interface Strategy {
    void update(Enemy state);
}
class Roamer implements Strategy {
    public void update(Enemy state) {
        //Roamer logic.
    }
}
class Chaser implements Strategy {
    public void update(Enemy state) {
        //Chaser logic.
    }
}

那么您将只需要每个特定策略的一个实例。例如,当前充当 Roamer 的池中的所有 Enemy 对象都将使用单个 Roamer 实例进行更新。这样可以避免将所有逻辑都保存在一个类中,这可能会变得异常复杂。

如果这不是一款 Android 游戏,那么您可以在 Enemy 类中放置一张地图,以保持一种策略需要而另一种策略不需要的敌人的状态。但是,由于我们必须避免可能触发垃圾收集器的分配,因此最好像您所说的那样在 Enemy 类中拥有所需的所有成员。

您可以在 Enemy 类中使​​用预先分配的数组来表示状态。例如,Chaser 将始终在状态数组的特定索引中保留对它正在追逐的 Player 的引用。然而,这变得非常复杂。

【讨论】:

    【解决方案2】:

    此外,更新的虚函数调用可能很慢。

    废话!进行虚函数调用的成本是 JIT 编译的应用程序中的几条指令。即使在解释模式下,我也只希望它需要更多的十几个本机指令。这在噪音中消失了......除非你每秒发出数千万次这样的调用。

    我会赞同@Laurence 回答的要点。只有在您验证(即通过测量)确实存在需要解决的性能问题后,才将时间花在性能改进上。如果你忽略了这一点,你就有可能使你的代码变得比它需要的更复杂和更难维护。而且你可能会在你天真地试图让它变得更好时让性能变得更糟......尤其是当 Android JIT 编译器变得更好时。

    【讨论】:

    • Android 最近才在 2.0 版中获得 JIT 编译。即使在那个版本中,它也没有为普通用户启用。它只是开发人员测试的一个选项。无论如何,许多手机永远不会升级到那么远。也就是说,在 Google I/O Real-Time Games 演讲中有一个关于 this 的图表,并且虚函数调用并没有那么糟糕。 JNI 和通过接口调用更糟糕:code.google.com/events/io/2009/sessions/…
    • “进行虚函数调用的成本是 JIT 编译的应用程序中的几条指令。”并且还可能发生缓存未命中,这比简单的几条额外指令的性能要差得多。
    【解决方案3】:

    您是否真的验证过没有内存池会出现性能问题?

    假设您确实需要使用内存池,为什么不为每种类型的对象设置一个并让它们“增长”到您需要的对象数量呢?如果对象类型之间的平衡随着时间的推移发生剧烈变化,则效率低下,但您描述的方法也存在效率低下(未使用的字段),并且看起来维护起来并不有趣。

    【讨论】:

    • > 您是否真的验证过没有内存池会出现性能问题?如果您经常解除分配工具,Android GC 可以随机运行大约 300 毫秒,因此如果您想要一个一致的例如30 帧/秒。 > 如果对象类型之间的平衡随着时间的推移发生剧烈变化,则效率低下,但您描述的方法也有效率低下 嗯。我也需要平衡这与虚函数调用的速度。
    【解决方案4】:

    您可以从此处复制框架内部的内存池(请参阅 Pool、Poolable、PoolableManager、Pools、FinitePool、SynchronizedPool):

    https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/util

    如果您这样做,请务必将它们放在您自己的包命名空间中。

    没有必要为了对象池而放弃面向对象。如果那里的代码仍然过于通用以至于让您感到害怕,请使用相同的想法为您的对象做一个简化的通用池。

    【讨论】:

      猜你喜欢
      • 2021-06-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-04-10
      • 2013-02-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多