使用快速固定块内存分配器替换 malloc/free






4.80/5 (24投票s)
使用 xmalloc/xfree 替换 malloc/free 比全局堆更快,并可防止堆碎片错误。
引言
自定义固定块分配器是专门的内存管理器,用于解决全局堆的性能问题。在文章 高效C++固定块内存分配器 中,我实现了一个分配器类来提高速度并消除堆内存碎片错误的可能性。在这篇最新文章中,Allocator
类被用作 xallocator
实现的基础,以替换 malloc()
和 free()
。
与大多数固定块分配器不同,xallocator
实现能够以完全动态的方式运行,而无需预先了解块的大小或数量。分配器会为您处理所有固定块的管理。它可以完全移植到任何基于PC的或嵌入式系统。此外,它还可以通过内存统计信息提供对动态使用的洞察。
在本文中,我将 C 库的 malloc
/free
替换为替代的固定内存块版本 xmalloc()
和 xfree()
。首先,我将简要解释底层的 Allocator
存储回收方法,然后介绍 xallocator
的工作原理。
使用 CMake 创建构建文件。CMake 是免费开源软件。支持 Windows、Linux 和其他工具链。有关更多信息,请参阅 **CMakeLists.txt** 文件。
查看 GitHub 以获取最新源代码
- 使用快速固定块内存分配器替换 malloc/free - 作者:David Lafreniere
存储回收
内存管理方案的基本理念是回收在对象分配期间获得的内存。一旦为对象创建了存储空间,它就不会被返回到堆。相反,内存会被回收,允许相同类型的另一个对象重用该空间。我实现了一个名为 Allocator
的类来表达这种技术。
当应用程序使用 Allocator
进行删除时,单个对象的内存块会被释放以便再次使用,但实际上并不会被释放回内存管理器。已释放的块会保留在一个称为自由列表(free-list)的链表中,以便再次分配给相同类型的另一个对象。在每次分配请求时,Allocator
首先检查自由列表是否有现有的内存块。只有在没有可用块时,才会创建一个新的块。根据 Allocator
的期望行为,存储可以来自全局堆或具有三种操作模式之一的静态内存池。
- 堆块
- 堆池
- 静态池
堆与池
当自由列表无法提供现有块时,Allocator
类能够从堆或内存池创建新块。如果使用池,您必须预先指定对象的数量。使用总对象数,将创建一个足够大的池来处理最大实例数。另一方面,从堆中获取块内存没有任何数量限制——您可以构建任意数量的新对象,只要存储允许。
堆块模式会在需要时从全局堆分配一个新的内存块给单个对象,以满足内存请求。去分配会将块放入自由列表以供以后重用。当自由列表为空时,从堆中创建新的块,使您不必设置对象限制。这种方法提供了动态操作,因为块的数量可以在运行时扩展。缺点是在块创建过程中会失去确定性执行。
堆池模式会从全局堆创建一个单个池来容纳所有块。当 Allocator
对象被构造时,使用 operator new
创建该池。然后,Allocator
在分配期间从池中提供内存块。
静态池模式使用单个内存池(通常位于静态内存中)来容纳所有块。静态内存池不是由 Allocator
创建的,而是由类的用户提供的。
堆池和静态池模式提供了稳定的分配执行时间,因为内存管理器从不参与获取单个块。这使得新的操作非常快速且确定。
Allocator
构造函数控制操作模式。
class Allocator
{
public:
Allocator(size_t size, UINT objects=0, CHAR* memory=NULL, const CHAR* name=NULL);
...
有关 Allocator
的更多信息,请参阅“高效C++固定块内存分配器”。
xallocator
xallocator
模块有六个主要 API
xmalloc
xfree
xrealloc
xalloc_stats
xalloc_init
xalloc_destroy
xmalloc()
等同于 malloc()
,用法完全相同。给定字节数,该函数返回一个指向请求大小内存块的指针。
void* memory1 = xmalloc(100);
内存块至少与用户请求的大小一样大,但由于固定块分配器的实现,实际上可能会更大。额外的超额分配内存称为“slack”,但通过微调块大小,可以最大限度地减少浪费,我将在文章后面解释。
xfree()
是 CRT 中 free()
的等效函数。只需将要释放的内存块的指针传递给 xfree()
,即可将其用于重用。
xfree(memory1);
xrealloc()
的行为与 realloc()
相同,它可以在保留内存块内容的同时扩展或收缩内存块。
char* memory2 = (char*)xmalloc(24);
strcpy(memory2, "TEST STRING");
memory2 = (char*)xrealloc(memory2, 124);
xfree(memory2);
xalloc_stats()
将分配器使用情况统计信息输出到标准输出流。输出提供了对正在使用的 Allocator
实例数量、正在使用的块、块大小等的见解。
xalloc_init()
必须在任何工作线程启动之前调用一次,或者在嵌入式系统的情况下,在操作系统启动之前调用。在 C++ 应用程序中,此函数会自动为您调用。但是,在某些情况下,手动调用 xalloc_init()
是可取的,通常是在嵌入式系统上,以避免自动 xalloc_init()
/xalloc_destroy()
调用机制带来的少量内存开销。
xalloc_destroy()
在应用程序退出时被调用,以清理任何动态分配的资源。在 C++ 应用程序中,此函数在应用程序终止时会自动调用。除非在仅在 C 文件中使用 xallocator
的程序中,否则您绝不能手动调用 xalloc_destroy()
。
现在,在 C++ 应用程序中何时调用 xalloc_init()
和 xalloc_destroy()
并不那么容易。问题出在 static
对象上。如果 xalloc_destroy()
调用得太早,当程序退出时静态对象的析构函数被调用时,xallocator
可能仍在使用中。考虑这个类
class MyClassStatic
{
public:
MyClassStatic()
{
memory = xmalloc(100);
}
~MyClassStatic()
{
xfree(memory);
}
private:
void* memory;
};
现在,在文件范围内创建该类的 static
实例。
static MyClassStatic myClassStatic;
由于该对象是 static
的,MyClassStatic
构造函数将在 main()
之前被调用,这没问题,我将在下面的“移植问题”部分解释。但是,析构函数将在 main()
退出后被调用,如果处理不当,这是不行的。问题变成了如何确定何时销毁 xallocator
动态分配的资源。如果在 main()
退出之前调用了 xalloc_destroy()
,那么当 ~MyClassStatic()
尝试调用 xfree()
时,xallocator
已经被销毁,从而导致错误。
解决方案的关键在于 C++ 标准中的一项保证
引用“在同一翻译单元中,在命名空间范围内定义的具有静态存储持续时间的动态初始化的对象,应按其定义在翻译单元中出现的顺序进行初始化。”
换句话说,static
对象的构造函数按照在文件(翻译单元)中定义的顺序被调用。销毁将颠倒该顺序。因此,xallocator.h 定义了一个名为 XallocInitDestroy
的类,并创建了它的一个 static
实例。
class XallocInitDestroy
{
public:
XallocInitDestroy();
~XallocInitDestroy();
private:
static INT refCount;
};
static XallocInitDestroy xallocInitDestroy;
构造函数跟踪创建的 static
实例的总数,并在第一次构造时调用 xalloc_init()
。
INT XallocInitDestroy::refCount = 0;
XallocInitDestroy::XallocInitDestroy()
{
// Track how many static instances of XallocInitDestroy are created
if (refCount++ == 0)
xalloc_init();
}
析构函数会在最后一个实例被销毁时自动调用 xalloc_destroy()
。
XallocDestroy::~XallocDestroy()
{
// Last static instance to have destructor called?
if (--refCount == 0)
xalloc_destroy();
}
在翻译单元中包含 xallocator.h 时,xallocInitDestroy
将首先声明,因为 #include
发生在用户代码之前。这意味着任何依赖 xallocator
的其他 static
用户类将在 #include “xallocator.h
” 之后声明。这保证了 ~XallocInitDestroy()
在所有用户 static
类析构函数执行之后被调用。使用此技术,xalloc_destroy()
在程序退出时安全地被调用,而不会有 xallocator
被过早销毁的风险。
XallocInitDestroy
是一个空类,因此大小为 1 字节。此功能的成本是每个包含 xallocator.h 的翻译单元 1 字节,但有以下例外。
- 在应用程序永不退出的嵌入式系统中,不需要此技术,除非使用了
STATIC_POOLS
模式。可以安全地移除对XallocInitDestroy
的所有引用,并且永远不需要调用xalloc_destroy()
。但是,您现在必须在main()
中手动调用xalloc_init()
,才能使用xallocator
API。 - 当
xallocator
包含在 C 翻译单元中时,不会创建XallocInitDestroy
的static
实例。在这种情况下,您必须在main()
中调用xalloc_init()
,并在main()
退出之前调用xalloc_destroy()
。
要启用或禁用 xallocator
的自动初始化和销毁,请使用以下 #define
#define AUTOMATIC_XALLOCATOR_INIT_DESTROY
在 PC 或具有类似配置的高 RAM 系统上,这 1 字节的开销微不足道,但它确保了在程序退出时 static
类实例中 xallocator
的安全操作。它还使您不必调用 xalloc_init()
和 xalloc_destroy()
,因为这些都是自动处理的。
重载 new 和 delete
为了使 xallocator
真正易于使用,我创建了一个宏来重载类中的 new
/delete
,并将内存请求路由到 xmalloc()
/xfree()
。只需在类定义中的任何位置添加宏 XALLOCATOR
即可。
class MyClass
{
XALLOCATOR
// remaining class definition
};
使用该宏,对您的类进行 new
/delete
操作将通过重载的 new
/delete
将请求路由到 xallocator
。
// Allocate MyClass using fixed block allocator
MyClass* myClass = new MyClass();
delete myClass;
一个巧妙的技巧是将 XALLOCATOR
放在继承层次结构的基类中,这样所有派生类都可以使用 xallocator
进行分配/去分配。例如,假设您有一个具有基类的 GUI 库。
class GuiBase
{
XALLOCATOR
// remaining class definition
};
任何 GuiBase
派生类(按钮、小部件等)现在在调用 new
/delete
时都会使用 xallocator
,而无需在每个派生类中添加 XALLOCATOR
。这是通过单个宏语句为整个层次结构启用固定块分配的强大方法。
代码实现
xallocator
依赖于多个 Allocator
实例来管理固定块;每个 Allocator
实例处理一个块大小。与 Allocator
一样,xallocator
设计为在堆块或静态池模式下运行。该模式由 xallocator.cpp 中的 STATIC_POOLS
定义控制。
#define STATIC_POOLS // Static pools mode enabled
在堆块模式下,xallocator
会根据请求的块大小在运行时动态创建 Allocator
实例和新块。默认情况下,xallocator
使用 2 的幂次块大小。8、16、32、64、128 等。这样,xallocator
不需要预先知道块大小,并提供了最大的灵活性。
xallocator
动态创建的 Allocator
实例的最大数量由 MAX_ALLOCATORS
控制。根据目标应用程序的需要增加或减少此数量。
#define MAX_ALLOCATORS 15
在静态池模式下,xallocator
依赖于在动态初始化期间(进入 main()
之前)创建的 Allocator
实例和静态内存池来满足内存请求。这消除了所有堆访问,但代价是块大小和池的大小是固定的,并且无法在运行时扩展。
使用 STATIC_POOLS
模式下的 Allocator
初始化很棘手。问题又在于用户类的静态构造函数,它们可能在构造/销毁过程中调用 xallocator
API。C++ 标准不保证在动态初始化期间翻译单元之间的静态构造函数调用顺序。然而,在执行任何 API 之前都必须初始化 xallocator
。因此,解决方案的第一部分是预先分配足够的静态内存以供每个 Allocator
实例使用。当然,每个分配器都可以根据需要使用不同的 MAX_BLOCKS
值。使用此模式,永远不会调用全局堆。
// Update this section as necessary if you want to use static memory pools.
// See also xalloc_init() and xalloc_destroy() for additional updates required.
#define MAX_ALLOCATORS 12
#define MAX_BLOCKS 32
// Create static storage for each static allocator instance
CHAR* _allocator8 [sizeof(AllocatorPool<CHAR[8], MAX_BLOCKS>)];
CHAR* _allocator16 [sizeof(AllocatorPool<CHAR[16], MAX_BLOCKS>)];
CHAR* _allocator32 [sizeof(AllocatorPool<CHAR[32], MAX_BLOCKS>)];
CHAR* _allocator64 [sizeof(AllocatorPool<CHAR[64], MAX_BLOCKS>)];
CHAR* _allocator128 [sizeof(AllocatorPool<CHAR[128], MAX_BLOCKS>)];
CHAR* _allocator256 [sizeof(AllocatorPool<CHAR[256], MAX_BLOCKS>)];
CHAR* _allocator396 [sizeof(AllocatorPool<CHAR[396], MAX_BLOCKS>)];
CHAR* _allocator512 [sizeof(AllocatorPool<CHAR[512], MAX_BLOCKS>)];
CHAR* _allocator768 [sizeof(AllocatorPool<CHAR[768], MAX_BLOCKS>)];
CHAR* _allocator1024 [sizeof(AllocatorPool<CHAR[1024], MAX_BLOCKS>)];
CHAR* _allocator2048 [sizeof(AllocatorPool<CHAR[2048], MAX_BLOCKS>)];
CHAR* _allocator4096 [sizeof(AllocatorPool<CHAR[4096], MAX_BLOCKS>)];
// Array of pointers to all allocator instances
static Allocator* _allocators[MAX_ALLOCATORS];
然后在动态初始化期间(通过 XallocInitDestroy()
)调用 xalloc_init()
时,使用放置 new 将每个 Allocator
实例初始化到先前预留的静态内存中。
extern "C" void xalloc_init()
{
lock_init();
#ifdef STATIC_POOLS
// For STATIC_POOLS mode, the allocators must be initialized before any other
// static user class constructor is run. Therefore, use placement new to initialize
// each allocator into the previously reserved static memory locations.
new (&_allocator8) AllocatorPool<CHAR[8], MAX_BLOCKS>();
new (&_allocator16) AllocatorPool<CHAR[16], MAX_BLOCKS>();
new (&_allocator32) AllocatorPool<CHAR[32], MAX_BLOCKS>();
new (&_allocator64) AllocatorPool<CHAR[64], MAX_BLOCKS>();
new (&_allocator128) AllocatorPool<CHAR[128], MAX_BLOCKS>();
new (&_allocator256) AllocatorPool<CHAR[256], MAX_BLOCKS>();
new (&_allocator396) AllocatorPool<CHAR[396], MAX_BLOCKS>();
new (&_allocator512) AllocatorPool<CHAR[512], MAX_BLOCKS>();
new (&_allocator768) AllocatorPool<CHAR[768], MAX_BLOCKS>();
new (&_allocator1024) AllocatorPool<CHAR[1024], MAX_BLOCKS>();
new (&_allocator2048) AllocatorPool<CHAR[2048], MAX_BLOCKS>();
new (&_allocator4096) AllocatorPool<CHAR[4096], MAX_BLOCKS>();
// Populate allocator array with all instances
_allocators[0] = (Allocator*)&_allocator8;
_allocators[1] = (Allocator*)&_allocator16;
_allocators[2] = (Allocator*)&_allocator32;
_allocators[3] = (Allocator*)&_allocator64;
_allocators[4] = (Allocator*)&_allocator128;
_allocators[5] = (Allocator*)&_allocator256;
_allocators[6] = (Allocator*)&_allocator396;
_allocators[7] = (Allocator*)&_allocator512;
_allocators[8] = (Allocator*)&_allocator768;
_allocators[9] = (Allocator*)&_allocator1024;
_allocators[10] = (Allocator*)&_allocator2048;
_allocators[11] = (Allocator*)&_allocator4096;
#endif
}
在应用程序退出时,会手动调用每个 Allocator
的析构函数。
extern "C" void xalloc_destroy()
{
lock_get();
#ifdef STATIC_POOLS
for (INT i=0; i<MAX_ALLOCATORS; i++)
{
_allocators[i]->~Allocator();
_allocators[i] = 0;
}
#else
for (INT i=0; i<MAX_ALLOCATORS; i++)
{
if (_allocators[i] == 0)
break;
delete _allocators[i];
_allocators[i] = 0;
}
#endif
lock_release();
lock_destroy();
}
隐藏分配器指针
删除内存时,xallocator
需要原始的 Allocator
实例,以便将去分配请求路由到正确的 Allocator
实例进行处理。与 xmalloc()
不同,xfree()
不接受大小,只使用 void*
参数。因此,xmalloc()
实际上是通过在内存块的未使用的部分添加额外的 4 字节(典型的 sizeof(Allocator*)
)来隐藏分配器指针。调用者获得指向块的客户端区域的指针,其中隐藏的分配器指针不会被覆盖。
extern "C" void *xmalloc(size_t size)
{
lock_get();
// Allocate a raw memory block
Allocator* allocator = xallocator_get_allocator(size);
void* blockMemoryPtr = allocator->Allocate(size);
lock_release();
// Set the block Allocator* within the raw memory block region
void* clientsMemoryPtr = set_block_allocator(blockMemoryPtr, allocator);
return clientsMemoryPtr;
}
当调用 xfree()
时,会从内存块中提取分配器指针,以便可以调用正确的 Allocator
实例来去分配该块。
extern "C" void xfree(void* ptr)
{
if (ptr == 0)
return;
// Extract the original allocator instance from the caller's block pointer
Allocator* allocator = get_block_allocator(ptr);
// Convert the client pointer into the original raw block pointer
void* blockPtr = get_block_ptr(ptr);
lock_get();
// Deallocate the block
allocator->Deallocate(blockPtr);
lock_release();
}
移植问题
当为目标平台实现锁时,xallocator
是线程安全的。提供的代码包含 Windows 锁。对于其他平台,您需要为 xallocator.cpp 中的四个函数提供锁实现:
lock_init()
lock_get()
lock_release()
lock_destroy()
选择锁时,请使用可用的最快的 OS 锁,以确保 xallocator
在多线程环境中尽可能高效地运行。如果您的系统是单线程的,则将上述每个函数的实现留空。
根据 xallocator
的使用方式,它可能会在 main()
之前被调用。这意味着 lock_get()
/lock_release()
可能会在 lock_init()
之前被调用。由于此时系统是单线程的,所以在操作系统启动之前不需要锁。但是,请确保 lock_get()
/lock_release()
在 lock_init()
未先调用时也能正常工作。例如,下面对 _xallocInitialized
的检查可确保正确行为,方法是在调用 lock_init()
之前跳过锁。
static void lock_get()
{
if (_xallocInitialized == FALSE)
return;
EnterCriticalSection(&_criticalSection);
}
减少 Slack
xallocator
可能返回大于请求数量的块大小,额外的未使用内存称为 slack。例如,对于 33 字节的请求,xallocator
返回一个 64 字节的块。额外的内存(64 - (33 + 4) = 27 字节)是 slack,未被使用。请记住,如果请求 33 字节,则还需要额外的 4 字节来存储块大小。因此,如果客户端请求 64 字节,实际上会使用 128 字节的分配器,因为需要 68 字节。
添加额外的分配器来处理非 2 的幂次块大小,可以提供更多块大小以最大限度地减少浪费。运行您的应用程序并使用一些临时调试代码来剖析您的 xmalloc()
请求大小。然后,为专门处理使用大量块的那些情况添加分配器块大小。
在下面的代码中,当请求 257 到 396 字节之间的块时,会创建一个块大小为 396 的 Allocator
实例。同样,请求 513 到 768 字节之间的块会导致一个 Allocator
来处理 768 字节的块。
// Based on the size, find the next higher powers of two value.
// Add sizeof(size_t) to the requested block size to hold the size
// within the block memory region. Most blocks are powers of two,
// however some common allocator block sizes can be explicitly defined
// to minimize wasted storage. This offers application specific tuning.
size_t blockSize = size + sizeof(Allocator*);
if (blockSize > 256 && blockSize <= 396)
blockSize = 396;
else if (blockSize > 512 && blockSize <= 768)
blockSize = 768;
else
blockSize = nexthigher<size_t>(blockSize);
通过少量微调,您可以根据应用程序的内存使用模式减少因 slack 造成的浪费存储。如果您不需要调整,并且仅使用基于 2 的幂次的块是可接受的,那么上面代码片段所需的唯一代码行是
size_t blockSize;
blockSize = nexthigher<size_t>(size + sizeof(Allocator*));
使用 xalloc_stats()
,可以轻松找到哪些分配器使用最多。
xallocator Block Size: 128 Block Count: 10001 Blocks In Use: 1
xallocator Block Size: 16 Block Count: 2 Blocks In Use: 2
xallocator Block Size: 8 Block Count: 1 Blocks In Use: 0
xallocator Block Size: 32 Block Count: 1 Blocks In Use: 0
Allocator 与 xallocator
使用 Allocator
的优点是分配器块大小恰好是对象的大小,并且最小块大小仅为 4 字节。缺点是 Allocator
实例是 private
的,只能被该类使用。这意味着固定块内存池不能轻易地与其他大小相似的块实例共享。由于内存池之间缺乏共享,这可能会浪费存储空间。
另一方面,xallocator
使用一系列不同的块大小来满足请求,并且是线程安全的。优点是通过 xmalloc
/xfree
接口共享各种大小的内存池,这可以节省存储空间,特别是当您针对特定应用程序调整块大小时。缺点是即使经过块大小调整,由于 slack,总会有一些浪费的存储。对于小对象,最小块大小为 8 字节,其中 4 字节用于自由列表指针,4 字节用于存储块大小。这对于大量小对象来说可能成为一个问题。
应用程序可以在同一程序中混合使用 Allocator
和 xallocator
,以最大限度地提高内存利用率,由设计者自行决定。
基准测试
将 xallocator
的性能与 Windows PC 上的全局堆进行基准测试,可以清晰地展示其速度。一项基本的测试是交错分配和去分配 20000 个 4096 和 2048 大小的块,以测试速度改进。所有测试都以最大编译器速度优化运行。有关确切算法,请参阅附带的源代码。
分配时间(毫秒)
分配器 | 模式 | Run | 基准测试时间(毫秒) |
全局堆 | 调试堆 | 1 | 1247 |
全局堆 | 调试堆 | 2 | 1640 |
全局堆 | 调试堆 | 3 | 1650 |
全局堆 | 发布堆 | 1 | 32.9 |
全局堆 | 发布堆 | 2 | 33.0 |
全局堆 | 发布堆 | 3 | 27.8 |
xallocator | 堆块 | 1 | 17.5 |
xallocator | 堆块 | 2 | 5.0 |
xallocator | 堆块 | 3 | 5.9 |
Windows 在调试器运行时使用调试堆。调试堆添加了额外的安全检查,从而降低了性能。发布堆快得多,因为禁用了检查。可以通过在 **Debugging > Environment** 项目选项中设置 _NO_DEBUG_HEAP=1
来在 Visual Studio 中禁用调试堆。
调试全局堆的速度可预测地最慢,大约为 1.6 秒。发布堆快得多,约为 30 毫秒。此基准测试非常简单,更实际的场景,具有不同的块大小和随机的新/删除间隔,可能会产生不同的结果。但是,基本观点得到了很好的说明;内存管理器比分配器慢,并且高度依赖于平台的实现。
xallocator
在堆块模式下运行得非常快,一旦自由列表用从堆中获得的块填充。请记住,堆块模式依赖于全局堆来获取新块,然后将它们回收并放入自由列表以供以后使用。运行 1 显示创建内存块的分配命中率为 17 毫秒。随后的基准测试以非常快的 5 毫秒完成,因为自由列表已完全填充。
正如基准测试所示,xallocator
非常高效,在 Windows PC 上比全局堆快约五倍。在 ARM STM32F4 CPU 上使用 Keil 编译器构建时,我看到了超过 10 倍的速度提升。
参考文章
- 高效 C++ 固定块内存分配器,作者:David Lafreniere
- 自定义 STL std::allocator 替换可提高性能,作者:David Lafreniere
结论
我参与的一个医疗设备有一个商业 GUI 库,该库广泛使用堆。内存请求的大小和频率无法预测或控制。以这种不受控制的方式使用堆在医疗设备上是不可取的,因此需要解决方案。幸运的是,GUI 库可以通过我们的自定义实现来替换 malloc()
和 free()
。xallocator
解决了堆的速度和碎片问题,使得 GUI 框架在该产品上成为一个可行的解决方案。
如果您的应用程序大量使用堆并导致性能下降,或者您担心堆碎片错误,集成 Allocator
/xallocator
可能会帮助解决这些问题。
历史
- 2016年3月11日
- 首次发布
- 2016年3月13日
- 更新了文章的基准测试部分
- 更新了
xallocator
以提高性能 - 更新了附带的源代码
- 2016年3月28日
- 更新了附带的源代码以修复
STATIC_POOLS
模式并帮助移植 - 更新了代码实现部分以反映新设计
- 更新了附带的源代码以修复
- 2016年4月3日
- 创建了参考文章部分
- 2016年4月9日
- 小 bug 修正。附带新源代码
- 2016年4月11日
- 根据反馈修复了 bug。附带新源代码
- 2016年4月15日
- 更新了“重载 new 和 delete”部分和代码片段
- 2024年1月21日
- 简化了代码以使用
std::mutex
- 简化了代码以使用