10.3. 动态参数变化处理

我们当前基于大小的内存池(size-based arena)实现是非常特定的:假设可以进行顺序分配,并且可以忽略(非常重要的)内存块释放后重用的问题。

对于基于大小的实现来说,一个重要的注意事项是:依赖于特定的大小(size)。因此,带着这个前提,我们当前的实现其实略带危险性。举个例子,程序发生了如下演化:我们引入了一个更强大、更“凶残”的 Orc 子类,比如:

class MeanOrc : public Orc {
  float attackBonus; // oops!
  // ...
}

乍看之下似乎没什么问题,但实际上可能已经破坏了某些关键逻辑。因为内存分配操作符是成员函数,会被派生类继承。所以,Tribe 类(也即 SizeBasedArena<Orc, Orc::NB_MAX>)所实现的分配策略是为大小为 sizeof(Orc) 的内存块设计的,但可能会(意外地)用于大小为 sizeof(MeanOrc) 的对象。

这注定会带来灾难性的后果。可以从两个方面来防止这种灾难性情况的发生,通过将 Orc 类标记为 final,彻底禁止继承:

class Orc final {
  // ...
}

这样,MeanOrc 无法再继承自 Orc。仍然可以通过组合(composition)等方式实现类似功能,从而绕过操作符继承的问题。

从 SizeBasedArena<T, N> 自身的角度来看,也可以决定只允许它用于 final 类型,例如:

// ...
#include <type_traits>

template <class T, std::size_t N>
class SizeBasedArena {
  static_assert(std::is_final_v<T>);
  // ...
};

然而,这部分做法并不一定适合所有人。有许多类型(例如基本类型)本身就不是 final,但也完全可以合理地用于基于大小的内存池(size-based arena)。因此,是否采用这种限制,完全取决于代码风格和需求。如果觉得不合适,那么这些约束也可以用文档说明(prose)的形式表达,而不是通过代码进行强制限制。

基于大小的内存池远不是内存池(memory arena)的唯一使用场景,还可以在“基于大小”这一前提下,衍生出许多变体,也可以采用不同的分配策略。

举个例子,假设游戏中引入了“萨满”(shamans),并且内存重用变成了一个现实需求。这时我们可能面临这样的情况:在整个程序运行期间,某一时刻最多只存在 Orc::NB_MAX 个 Orc 对象,但整个程序生命周期中创建的 Orc 对象总数可能会超过这个数字。

这种情况下,需要考虑以下几点:

对于“我们的代码库中哪种方法最好?”这类问题,其答案一部分技术性,一部分权衡性(或者说“政治性”):某些策略可能会让分配变快,却让释放变慢;某些策略可以让分配速度变得可预测,但却带来更大的内存开销等。关键在于:确定当前的场景中哪些权衡是最合适的,并通过实际测量来保证获得了预期的性能提升。如果无法实现比标准库当前所做的更好的性能表现,请毫不犹豫地使用标准库提供的内存管理机制!毕竟,它们通常已经非常高效、稳定且经过了广泛测试。自定义内存管理,只应在有明确性能瓶颈,且具备足够先验知识的情况下才进行考虑。