以下为个人学习笔记整理。参考 PhysX SDK 3.4.0 文档,部分代码可能来源于更高版本。
# PhysX——Memory Management 篇 (未完待续)
PhysX 所有对象的分配都是基于 Foundation 内的 PxAllocatorCallback 完成,这里以 PsPool 作为按理简单介绍一下
先来看看 Pool 的构造函数,这里稍微有些绕:
// 通过泛型模板定义好该类型的分配器 Alloc | |
template <class T, class Alloc = typename AllocatorTraits<T>::Type> | |
class PoolBase : public UserAllocated, public Alloc | |
{ | |
PX_NOCOPY(PoolBase) | |
protected: | |
PoolBase(const Alloc& alloc, uint32_t elementsPerSlab, uint32_t slabSize) | |
: Alloc(alloc), mSlabs(alloc), mElementsPerSlab(elementsPerSlab), mUsed(0), mSlabSize(slabSize), mFreeElement(0) | |
{ | |
PX_COMPILE_TIME_ASSERT(sizeof(T) >= sizeof(size_t)); | |
} | |
} |
这里接收一个 T 类型的模板类,并根据模板类声明对应的模板类内存分配器 Alloc。 AllocatorTraits<T>::Type
分为两类,这个后面再详细介绍:
- 基于名字查找的分配 ——NamedAllocator
- 不基于名字查找的分配 ——ReflectionAllocator。
template <typename T> | |
struct AllocatorTraits | |
{ | |
#if PX_USE_NAMED_ALLOCATOR | |
typedef NamedAllocator Type; | |
#else | |
typedef ReflectionAllocator<T> Type; | |
#endif | |
}; |
不论哪一个,其提供的 allocate 函数都会最终指向 Foundation 的 allocate 函数:
// NamedAllocator | |
void* NamedAllocator::allocate(size_t size, const char* filename, int line) | |
{ | |
if(!size) | |
return 0; | |
Foundation::Mutex::ScopedLock lock(getMutex()); | |
const AllocNameMap::Entry* e = getMap().find(this); | |
PX_ASSERT(e); | |
return getAllocator().allocate(size, e->second, filename, line); | |
} | |
// ReflectionAllocator<T> | |
void* allocate(size_t size, const char* filename, int line) | |
{ | |
return size ? getAllocator().allocate(size, getName(), filename, line) : 0; | |
} | |
//getAllocator 实际上是从全局单例 Foundation 获取分配器对象 | |
PxAllocatorCallback& getAllocator() | |
{ | |
return getFoundation().getAllocator(); | |
} |
细心的小伙伴应该发现了两者的细微差别:NamedAllocator 里使用了名字字典并且为了保证线程安全还加了锁,这个我们后面再讲。
下面先来看看 Foundation 的 getAllocator 里面获取到的到底是什么:🤔
// Foundation 的分配器获取实现如下: | |
PxAllocatorCallback& getAllocator() | |
{ | |
return mBroadcastingAllocator; | |
} | |
// 分配器的注册可以由开发者指定,一般情况下都是使用默认分配器 PxDefaultAllocator | |
physx::PxFoundation* PxCreateFoundation(physx::PxU32 version, physx::PxAllocatorCallback& allocator, | |
physx::PxErrorCallback& errorCallback) | |
{ | |
return physx::shdfnd::Foundation::createInstance(version, errorCallback, allocator); | |
} | |
// Foundation 自身是个单例 | |
Foundation* Foundation::createInstance(PxU32 version, PxErrorCallback& errc, PxAllocatorCallback& alloc) | |
{ | |
if(!mInstance) | |
{ | |
mInstance = reinterpret_cast<Foundation*>(alloc.allocate(sizeof(Foundation), "Foundation", __FILE__, __LINE__)); | |
// ... | |
PX_PLACEMENT_NEW(mInstance, Foundation)(errc, alloc); | |
// ... | |
} | |
return 0; | |
} | |
//Foundation 的构造函数,其中 mAllocatorCallback 和 mBroadcastingAllocator 都是通过 alloc 对象进行内存分配 | |
Foundation::Foundation(PxErrorCallback& errc, PxAllocatorCallback& alloc) : | |
mAllocatorCallback(alloc), // 在这里被初始化 | |
mBroadcastingAllocator(alloc, errc) // ... | |
{} |
getAllocator 返回的是一个 PxAllocatorCallback 的对象,在初始化 Foundation 的时候被设置。
一般情况下会使用默认的分配器进行初始化,当然也可以开发者指定:
class PxDefaultAllocator : public PxAllocatorCallback | |
{ | |
public: | |
void* allocate(size_t size, const char*, const char*, int) | |
{ | |
//linux 用 malloc, window 用 _aligned_malloc, 保证 16 字节对齐 | |
void* ptr = platformAlignedAlloc(size); | |
PX_ASSERT((reinterpret_cast<size_t>(ptr) & 15)==0); | |
return ptr; | |
} | |
void deallocate(void* ptr) | |
{ | |
platformAlignedFree(ptr); | |
} | |
}; |
# 内存分配器
前面简单介绍了 Physx 的两种内存分配器,这里在做一个小小的展开🪂
# NamedAllocator
名字查找的分配器会在 CHECK 或者 DEBUG 模式下开启:
#if PX_DEBUG || PX_CHECKED | |
#define PX_USE_NAMED_ALLOCATOR 1 | |
#else | |
#define PX_USE_NAMED_ALLOCATOR 0 | |
#endif |
由于名字查找的分配模式需要操作 Foundation 内部的一个名字字典,每次查改都需要加锁,所以效率会慢一些:
PX_INLINE AllocNameMap& getMap() | |
{ | |
return getFoundation().getNamedAllocMap(); | |
} | |
// 初始化的时候新增(加锁) | |
NamedAllocator::NamedAllocator(const char* name) | |
{ | |
Foundation::Mutex::ScopedLock lock(getMutex()); | |
getMap().insert(this, name); | |
} | |
// 内存分配的时候查找(加锁) | |
void* NamedAllocator::allocate(size_t size, const char* filename, int line) | |
{ | |
if(!size) | |
return 0; | |
Foundation::Mutex::ScopedLock lock(getMutex()); | |
const AllocNameMap::Entry* e = getMap().find(this); | |
PX_ASSERT(e); | |
return getAllocator().allocate(size, e->second, filename, line); | |
} |
# ReflectionAllocator
初见 Reflection 还以为通过反射机制来实现内存分配,想着蛮吊的。
细看之下越发的疑惑,压根没有任何反射逻辑,单纯是不使用名字查找,且名字通过 C++ 内置的 typeid(T).name()
自动生成。🤕
template <typename T> | |
class ReflectionAllocator | |
{ | |
static const char* getName() | |
{ | |
// ... | |
return typeid(T).name(); | |
} | |
public: | |
ReflectionAllocator(const char* = 0) | |
{ | |
} | |
void* allocate(size_t size, const char* filename, int line) | |
{ | |
return size ? getAllocator().allocate(size, getName(), filename, line) : 0; | |
} | |
void deallocate(void* ptr) | |
{ | |
if(ptr) | |
getAllocator().deallocate(ptr); | |
} | |
}; |
效率上面比 NamedAllocator 高的原因大概是省略了名字字典的操作,避免额外的加锁。
另外感叹一下这个类做成模板类看起来仅仅只为了获取 typeid(T)
,属实有些大材小用。
todo...