- 浏览: 33110 次
- 性别:
- 来自: 大连
最新评论
再探小型对象分配技巧(Small-Object Allocation)
/*
* References, 参考文献:
* [1]. Alexandrescu, Andrei. "Modern C++ Design: Generic Programming and Design
* Patterns Applied". Copyright (c) 2001. Addison-Wesley.
*
* 本文研究的源代码取自loki库SmallObj.h & SmallObj.cpp
*/
#include <boost/progress.hpp> #include <vector> using namespace boost; using namespace std; struct SmallObj { char c; }; int main() { progress_timer t; SmallObj *s = new SmallObj(); //case 1. vector<char> vc; //case 2. vector<string>vs; //case 3. vector<int> *vi = new vector<int>(1); //case 4. std::cout << t.elapsed() << std::endl; }
利用boost的progress_timer库编了一个小测试,看看c++默认的new操作大概需要多长时间.
机器配置:2.26 GHz Intel Core 2 Duo, 4GB 1067 MHz DDR3, Mac OS 10.7.3.
1. 单独执行case 1时,耗时平均3微妙,小对象内存的动态内存分配时间;
2. 同时执行2,3时,耗时最低3微妙,最多4微妙,通过std::alloc分配小对象内存;这种情况下,通常case 3是从case 2 已经分配的segregated freelist中得到分配的内存快,耗时应该很低。
3. 只执行case 4时,最低耗时4微妙,最多6微妙。
按照[1]的说法,系统缺省的free store分配器速度很慢,并且跟loki编写小型对象分配器比较,执行速度慢一个等量级。下面就看看loki的Small-Object Allocator是怎么实现的,有哪些优化技巧在实现中。
loki的小型对象分配器一共分为四层,处于最底层的是chunk struct。每一个chunk对象包含并管理一大块内存,这一大块内存本身包含整数个固定大小的区块(block)。chunk内包含逻辑信息,使用者可根据这些逻辑信息分配和归还区块,当chunk中不再剩余blocks时,分配失败并传回零。
第二层是FixedAllocator,其对象以chunk为构件。FixedAllocator主要用来满足那些“累计总量超过chunk容量”的内存分配请求。FixedAllocator会通过一个vector将chunks组合起来以达到目的。如果新的内存请求出现,但vector中的chunk都被占用了,此时FixedAllocator会产生一块新的chunk,并将它添加到vector中,再由chunk满足需求。
第三层SmallObjAllocator提供的通用性的分配归还函数,此对象拥有数个FixedAllocator对象,每一个负责分配某特定大小的对象。根据申请的bytes个数不同,SmallObjAllocator对象会将内存分配申请分发给辖内某个FixedAllocator。如果请求数量过大,会转发给系统提供的::operator new。
最后一层是SmallObject,它包装了FixedAllocator,以便向C++ Classes提供封装良好的分配服务。SmallObject重载了operator new 和 operator delete,将任务转给SmallObjAllocator对象去完成。
Chunk
Chunk的定义如下
struct Chunk { void Init(std::size_t blockSize, unsigned char blocks); void* Allocate(std::size_t blockSize); void Deallocate(void* p, std::size_t blockSize); void Reset(std::size_t blockSize, unsigned char blocks); void Release(); unsigned char* pData_; unsigned char firstAvailableBlock_, blocksAvailable_; };
pData指向被管理内存本身,除此以外chunk还保存一下整数值:
- firstAvailableBlock_, chunk内的第一个可用区块的索引号
- blocksAvailable_, chunk内的可用区块总数
因为firstAvailableBlock_, blocksAvailable_的类型都是unsigned char,因此一个chunk在一部8-bit char机器上无法拥有255个以上的区块。如果block未被使用,就拿第一个字节放置“下一个未被使用的区块”的索引号。由于firstAvailableBlock_已经持有第一个block的索引号,因此我们便有了一个由“可用区块”组成的freelist,无须占用额外内存。
void FixedAllocator::Chunk::Init(std::size_t blockSize, unsigned char blocks) { assert(blockSize > 0); assert(blocks > 0); // Overflow check assert((blockSize * blocks) / blockSize == blocks); pData_ = new unsigned char[blockSize * blocks]; Reset(blockSize, blocks); } void FixedAllocator::Chunk::Reset(std::size_t blockSize, unsigned char blocks) { assert(blockSize > 0); assert(blocks > 0); // Overflow check assert((blockSize * blocks) / blockSize == blocks); firstAvailableBlock_ = 0; blocksAvailable_ = blocks; unsigned char i = 0; unsigned char* p = pData_; for (; i != blocks; p += blockSize) { *p = ++i; } }
其中里面有两个技巧特别需要思考一下:
1. 将可用block的第一个字节记录索引号,加firstAvailableBlock_,形成freelist的好处是什么?
2. 为什么要将chunk管理的block数量用unsigned char类型的数值上限(255)加以限制。
回答:
1. 这种freelist的设计技巧在于,融于数据结构本身,无须占用额外内存,且提供了一种高效的方法chunk中可用block。
2. 假定chunk改为class template,如下
template<typename T> struct Chunk { void Init(std::size_t blockSize, T blocks); void Release(); void * Allocate(std::size_t blockSize); void Deallocate(void *p, std::size_t blockSize); unsigned char *pData_; T firstAvailableBlock_, blocksAvailable_; }; template<typename T> void Chunk<T>::Init(std::size_t blockSize, T blocks){ pData_ = new unsigned char[blockSize * blocks]; firstAvailableBlock_ = 0; blocksAvailable_ = blocks; T i = 0; for (; i != blocks; p+=blockSize) *p = ++i; }
按此修改后,如果T被unsigned short替换的话,最高值是65535, 但是我们无法分配比sizeof(unsigned short)小的内存,但是这不是十分大的问题,可以将比之小的内存对齐成sizeof(unsigned short)大小再分配;当然这会引起Internal fragmentation。
另一个问题是齐位问题,假如分配block大小是5个字节,那么区块的索引该用什么类型呢?unsigned short,还是unsigned int,在将指向这样一个5字节block的指针转换为unsigned int,会引发不确定的行为。即使按照特定的字节序进行读取这5个字节中的4个字节转换为unsigned int,也要费一番周折,付出的开销或许已经抵消了由此设计而带来的效率提升。
所以限定一下blocks的数量为255,保持在类型unsigned char的数值上限,不失为一个明智之举。一来,chunks不是那么大,二来char的大小是1个字节,无齐位问题,即使原始内存的指针也是指向unsigned char的。
分配函数Allocate()的动作就是取出firstAvailableBlock_所代表的区块,然后调整firstAvailableBlock_,使其指向下一个可用区块。
void* FixedAllocator::Chunk::Allocate(std::size_t blockSize) { if (!blocksAvailable_) return 0; assert((firstAvailableBlock_ * blockSize) / blockSize == firstAvailableBlock_); unsigned char* pResult = pData_ + (firstAvailableBlock_ * blockSize); firstAvailableBlock_ = *pResult; --blocksAvailable_; return pResult; }
注意这个allocate的成本很小,因为不需要查找。
归还函数Deallocate:
void FixedAllocator::Chunk::Deallocate(void* p, std::size_t blockSize) { assert(p >= pData_); unsigned char* toRelease = static_cast<unsigned char*>(p); // Alignment check assert((toRelease - pData_) % blockSize == 0); *toRelease = firstAvailableBlock_; firstAvailableBlock_ = static_cast<unsigned char>( (toRelease - pData_) / blockSize); // Truncation check assert(firstAvailableBlock_ == (toRelease - pData_) / blockSize); ++blocksAvailable_; }
配合看Allocate和Deallocate函数实现时,需要注意几点:
1. pData永远都是指向分配chunks的起始地址。
2. firstAvailableBlock_永远记录的是第一个可用block的索引号
3. 也是最重要的一点,(经过多次allocation和deallocation之后)每个block的第一个字节保存的是下一个可用block的索引号。
4. 一次内存请求只是一个block size,不能一次分配多个block。
FixedAllocator
小型对象分配器的第二层由FixedAllocator构成。FixedAllocator负责分配和归还“特定大小的block”,其大小不受限于chunk,因为FixedAllocator将新建的chunk对象放在一个vector中。任何一个内存请求,会找出一个适当的chunk满足,如果都被占用了,就新建一个chunk加入到vector中。下面是FixedAllocator的定义:
class FixedAllocator{ private: void DoDeallocate(void *p); bool MakeNewChunk(void); Chunk * VicinityFind(void *p) const; FixedAllocator(const FixedAllocator&); //copy ctor, but not implemented; FixedAllocator& operator=(const FixedAllocator&);// copy assignment, not implemented; typedef std::vector<Chunk> Chunks; typedef Chunks::iterator ChunkIter; typedef Chunks::const_iterator ChunkCIter; static unsigned char MinObjectsPerChunk_; // Fewest # of objects managed by a chunk; static unsigned char MaxObjectsPerChunk_; // maximum # of objects manged by a chunk; std::size_t blockSize_; //FixedAllocator manages chunks owning blockSize_ specific blocks only. unsigned char numBlocks_; Chunks chunks; // Container of Chunks Chunk *allocChunk_; //ptr to chunk used for last/next allocation. Chunk *deallocChunk_; // ptr to chunk used for last/next deallocation. Chunk * emptyChunk; // ptr to the only empty chunk if there is one, else null... public: FixedAllocator(); ~FixedAllocator(); void Initialize(std::size_t blockSize, std::size_t pageSize); void *Allocate(void); bool Deallocate(void *p, Chunk *hint); inline std::size_t BlockSize() const { return blockSize_; } bool TrimEmptyChunk(void); // releases the memory used by empty chunk. bool TrimChunkList(void); // Returns count of empty Chunks held by this allocator. std::size_t CountEmptyChunks(void) const; bool IsCorrupt(void) const; const Chunk * HasBlock(void *p) const; inline Chunk * HasBlock(void *p){ return const_cast<Chunk *>( const_cast< const FixedAllocator *>( this )->HasBlock(p) ); } };
FixedAllocator::allocate()
其中,allocChunk_指针指向“最近一次分配所使用的chunk”。每次分配请求都会先查询该指针所指的chunk,如果尚有空闲空间,分配请求将由此chunk分配获得。否则会触发一次线性查找。无论上述哪种情况,allocChunk_都会更新,指向刚找到的或新添加的chunk。这样可以提高下次分配速度。以下是loki_0.1.7的FixedAlloctor::Allocate(void)的实现:
void * FixedAllocator::Allocate( void ) { if ( ( NULL == allocChunk_ ) || allocChunk_->IsFilled() ) //初始状态或者最近一次分配所用的chunk已被占用 { if ( NULL != emptyChunk_ ) //有可用的chunk,并且是空的,直接从这个chunk分配内存 { allocChunk_ = emptyChunk_; emptyChunk_ = NULL; } else //只能遍历chunks,看是否有空闲chunk可用 { for ( ChunkIter i( chunks_.begin() ); ; ++i ) { if ( chunks_.end() == i ) // 如果没有,只能创建一个新的chunk,并加入到chunks, // 同时修改allocChunk_,deallocChunk_指针 { if ( !MakeNewChunk() ) return NULL; break; } if ( !i->IsFilled() ) //如果chunks中某个chunk(first-fit)有空闲block,分配内存。 { allocChunk_ = &*i; break; } } } } else if ( allocChunk_ == emptyChunk_) //无空emptyChunk时,修改指针emptyChunk_,置空。 emptyChunk_ = NULL; void * place = allocChunk_->Allocate( blockSize_ ); return place; }
FixedAllocator::deallocated(void *p, chunk *hint);
内存归还(deallocate)比较麻烦,因为不知道待还block属于哪个chunk的。是的,我们可以遍历chunks, 检查指针是否落在pData_和pData_+blockSize_ * numBlocks_之间。找到对应的chunk后,就在这个chunk内做deallocated动作。不过这么做,需要耗费线性时间归还内存。因此loki做了一些优化,设一个成员变量deallocChunk_指针,指向归还动作所用的最后那个chunk对象。任何归还动作都必须先检查这个deallocChunk_所指向的chunk,如果是错误的chunk,再进行线性搜索,搜到以后做deallocate动作,最后修改deallocChunk_指针。
bool FixedAllocator::Deallocate( void * p, Chunk * hint ) { Chunk * foundChunk = ( NULL == hint ) ? VicinityFind( p ) : hint; if ( NULL == foundChunk ) return false; deallocChunk_ = foundChunk; DoDeallocate(p); return true; } Chunk * FixedAllocator::VicinityFind( void * p ) const { if ( chunks_.empty() ) return NULL; assert(deallocChunk_); const std::size_t chunkLength = numBlocks_ * blockSize_; Chunk * lo = deallocChunk_; Chunk * hi = deallocChunk_ + 1; const Chunk * loBound = &chunks_.front(); const Chunk * hiBound = &chunks_.back() + 1; // Special case: deallocChunk_ is the last in the array if (hi == hiBound) hi = NULL; //边界条件 for (;;) { if (lo) { if ( lo->HasBlock( p, chunkLength ) ) return lo; if ( lo == loBound ) { lo = NULL; if ( NULL == hi ) break; } else --lo; } if (hi) { if ( hi->HasBlock( p, chunkLength ) ) return hi; if ( ++hi == hiBound ) { hi = NULL; if ( NULL == lo ) break; } } } return NULL; } void FixedAllocator::DoDeallocate(void* p) { // Show that deallocChunk_ really owns the block at address p. assert( deallocChunk_->HasBlock( p, numBlocks_ * blockSize_ ) ); // Either of the next two assertions may fail if somebody tries to // delete the same block twice. assert( emptyChunk_ != deallocChunk_ ); assert( !deallocChunk_->HasAvailable( numBlocks_ ) ); // prove either emptyChunk_ points nowhere, or points to a truly empty Chunk. assert( ( NULL == emptyChunk_ ) || ( emptyChunk_->HasAvailable( numBlocks_ ) ) ); // call into the chunk, will adjust the inner list but won't release memory deallocChunk_->Deallocate(p, blockSize_); if ( deallocChunk_->HasAvailable( numBlocks_ ) ) //如果deallocChunk是个空chunk { assert( emptyChunk_ != deallocChunk_ ); // deallocChunk_ is empty, but a Chunk is only released if there are 2 // empty chunks. Since emptyChunk_ may only point to a previously // cleared Chunk, if it points to something else besides deallocChunk_, // then FixedAllocator currently has 2 empty Chunks. if ( NULL != emptyChunk_ ) { // If last Chunk is empty, just change what deallocChunk_ // points to, and release the last. Otherwise, swap an empty // Chunk with the last, and then release it. Chunk * lastChunk = &chunks_.back(); if ( lastChunk == deallocChunk_ ) deallocChunk_ = emptyChunk_; else if ( lastChunk != emptyChunk_ ) std::swap( *emptyChunk_, *lastChunk ); assert( lastChunk->HasAvailable( numBlocks_ ) ); lastChunk->Release(); chunks_.pop_back(); if ( ( allocChunk_ == lastChunk ) || allocChunk_->IsFilled() ) allocChunk_ = deallocChunk_; } emptyChunk_ = deallocChunk_; } // prove either emptyChunk_ points nowhere, or points to a truly empty Chunk. assert( ( NULL == emptyChunk_ ) || ( emptyChunk_->HasAvailable( numBlocks_ ) ) ); }
注意VicinityFind函数,里面有两个迭代器,lo和hi,(不明白为什么不用std::iterator包装一下)每次迭代分别向上和向下进行。(这样实现,有点divide-and-conquer的感觉哈)。
在DoDeallocate函数实现中,当deallocChunk_是空的时候,并且emptyChunk_也存在时,说明FixedAllocator此时很有同时存在两个emptyChunk了(除了deallcoChunk_与emptyChunk_都指向同一个chunk时之外),这种情况势必要释放一个emptyChunk。根据语句 lastChunk->Release(); chunks_.pop_back();可以判断出释放动作发生在chunks vector的最后一个chunk对象上(lastChunk_)。因此,在此之前需要根据以下两种具体情况修改deallocChunk_和emptyChunk_指针:
1. 如果deallocChunk指向最后一个chunk,那么将deallocChunk指向原emptyChunk。
2. 如果lastChunk不是emptyChunk指向的那个chunk,那么交换两个chunk(不是指针交换)。
这么做的原因是为了避免一种边界条件(allocChunk_和deallocChunk_均指向同一个chunk,且该chunk已无空闲空间)。如果按照常规的allocate()的逻辑,会新建一个chunk,并加入到chunks中,并在新建的chunk上完成内存分配。可以恰好,这个刚分配完的内存又要归还。这时deallocChunk_找到新建的chunk,完成归还动作后,发现是空的chunk,就释放掉这个新建的chunk。如果这个逻辑如此反复,性能损耗不可小觑。所以只有在发现两个空的chunk时,才会归还其中之一。
SmallObjAllocator
loki的allocator的第三层便是这个SmallObjAllocator,它能够分配任意大小的对象。SmallObjAllocator聚集了N个FixedAllocator。当SmallObjAllocator接收到一个分配请求时,会将请求分配给最佳匹配的FixedAllocator,否则就转给缺省的::operator new. 所以SmallObjAllocator的声明如下:
class LOKI_EXPORT SmallObjAllocator { protected: SmallObjAllocator( std::size_t pageSize, std::size_t maxObjectSize, std::size_t objectAlignSize ); ~SmallObjAllocator( void ); public: void * Allocate( std::size_t size, bool doThrow ); void Deallocate( void * p, std::size_t size ); //多了一个参数,表明待还内存大小 void Deallocate( void * p ); inline std::size_t GetMaxObjectSize() const { return maxSmallObjectSize_; } /// Returns # of bytes between allocation boundaries. inline std::size_t GetAlignment() const { return objectAlignSize_; } bool TrimExcessMemory( void ); bool IsCorrupt( void ) const; private: SmallObjAllocator( void ); SmallObjAllocator( const SmallObjAllocator & ); SmallObjAllocator & operator = ( const SmallObjAllocator & ); Loki::FixedAllocator * pool_; //array instead of vector. (modification) /// Largest object size supported by allocators. 超过这值就转发到默认的::operator new const std::size_t maxSmallObjectSize_; /// Size of alignment boundaries. const std::size_t objectAlignSize_; };
void * SmallObjAllocator::Allocate( std::size_t numBytes, bool doThrow )
void * SmallObjAllocator::Allocate( std::size_t numBytes, bool doThrow ) { if ( numBytes > GetMaxObjectSize() ) return DefaultAllocator( numBytes, doThrow ); assert( NULL != pool_ ); if ( 0 == numBytes ) numBytes = 1; const std::size_t index = GetOffset( numBytes, GetAlignment() ) - 1; const std::size_t allocCount = GetOffset( GetMaxObjectSize(), GetAlignment() ); (void) allocCount; assert( index < allocCount ); FixedAllocator & allocator = pool_[ index ]; assert( allocator.BlockSize() >= numBytes ); assert( allocator.BlockSize() < numBytes + GetAlignment() ); void * place = allocator.Allocate(); if ( ( NULL == place ) && TrimExcessMemory() ) place = allocator.Allocate(); if ( ( NULL == place ) && doThrow ) { #ifdef _MSC_VER throw std::bad_alloc( "could not allocate small object" ); #else // GCC did not like a literal string passed to std::bad_alloc. // so just throw the default-constructed exception. throw std::bad_alloc(); #endif } return place; }
pool_是个FixedAllocator数组,为了能够简单而且高效地查找到哪个FixedAllocator负责哪个大小的内存块,所以数组下标index处理index大小的内存块。但是事情总是没有那么完美的,不是吗?也许某个应用程序只需产生4字节和32字节的大小的两种对象,再没其他的了。但是pool_还是要分配负责除此两个大小以外的FixedAllocator。
void SmallObjAllocator::Deallocate( void * p, std::size_t numBytes )
void SmallObjAllocator::Deallocate( void * p, std::size_t numBytes ) { if ( NULL == p ) return; if ( numBytes > GetMaxObjectSize() ) { DefaultDeallocator( p ); return; } assert( NULL != pool_ ); if ( 0 == numBytes ) numBytes = 1; const std::size_t index = GetOffset( numBytes, GetAlignment() ) - 1; const std::size_t allocCount = GetOffset( GetMaxObjectSize(), GetAlignment() ); (void) allocCount; assert( index < allocCount ); FixedAllocator & allocator = pool_[ index ]; assert( allocator.BlockSize() >= numBytes ); assert( allocator.BlockSize() < numBytes + GetAlignment() ); const bool found = allocator.Deallocate( p, NULL ); (void) found; assert( found ); } void SmallObjAllocator::Deallocate( void * p ) //这个函数因为只有一个待还区块的指针,所以不得已需要遍历FixedAllocator,找到对应负责的FixedAllocator。 { if ( NULL == p ) return; assert( NULL != pool_ ); FixedAllocator * pAllocator = NULL; const std::size_t allocCount = GetOffset( GetMaxObjectSize(), GetAlignment() ); Chunk * chunk = NULL; for ( std::size_t ii = 0; ii < allocCount; ++ii ) { chunk = pool_[ ii ].HasBlock( p ); if ( NULL != chunk ) { pAllocator = &pool_[ ii ]; break; } } if ( NULL == pAllocator ) { DefaultDeallocator( p ); return; } assert( NULL != chunk ); const bool found = pAllocator->Deallocate( p, chunk ); (void) found; assert( found ); }
SmallObjAllocator只是FixedAllocator的简单包装。
SmallObject
第四层,它将第三层提供的功能做更便捷于是用的包装。SmallObject重载系统默认的::operator new 和 ::operator delete。 这样,每生成一个SmallObject对象,重载后的行为便会将分配请求发送给底层的FixedAllocator。所以定义如下
template < template <class, class> class ThreadingModel = LOKI_DEFAULT_THREADING_NO_OBJ_LEVEL, std::size_t chunkSize = LOKI_DEFAULT_CHUNK_SIZE, std::size_t maxSmallObjectSize = LOKI_MAX_SMALL_OBJECT_SIZE, std::size_t objectAlignSize = LOKI_DEFAULT_OBJECT_ALIGNMENT, template <class> class LifetimePolicy = LOKI_DEFAULT_SMALLOBJ_LIFETIME, class MutexPolicy = LOKI_DEFAULT_MUTEX > class SmallObject : public SmallObjectBase< ThreadingModel, chunkSize, maxSmallObjectSize, objectAlignSize, LifetimePolicy, MutexPolicy > { public: virtual ~SmallObject() {} protected: inline SmallObject( void ) {} private: /// Copy-constructor is not implemented. SmallObject( const SmallObject & ); /// Copy-assignment operator is not implemented. SmallObject & operator = ( const SmallObject & ); }; // end class SmallObject template < template <class, class> class ThreadingModel, std::size_t chunkSize, std::size_t maxSmallObjectSize, std::size_t objectAlignSize, template <class> class LifetimePolicy, class MutexPolicy > class SmallObjectBase { public: typedef AllocatorSingleton< ThreadingModel, chunkSize, maxSmallObjectSize, objectAlignSize, LifetimePolicy > ObjAllocatorSingleton; private: typedef ThreadingModel< ObjAllocatorSingleton, MutexPolicy > MyThreadingModel; typedef typename ObjAllocatorSingleton::MyAllocatorSingleton MyAllocatorSingleton; public: static void * operator new ( std::size_t size, const std::nothrow_t & ) throw () { typename MyThreadingModel::Lock lock; (void)lock; // get rid of warning return MyAllocatorSingleton::Instance().Allocate( size, false ); } /// Placement single-object new merely calls global placement new. inline static void * operator new ( std::size_t size, void * place ) { return ::operator new( size, place ); } static void operator delete ( void * p, std::size_t size ) throw () { typename MyThreadingModel::Lock lock; (void)lock; // get rid of warning MyAllocatorSingleton::Instance().Deallocate( p, size ); } static void operator delete ( void * p, const std::nothrow_t & ) throw() { typename MyThreadingModel::Lock lock; (void)lock; // get rid of warning MyAllocatorSingleton::Instance().Deallocate( p ); } /// Placement single-object delete merely calls global placement delete. inline static void operator delete ( void * p, void * place ) { ::operator delete ( p, place ); } protected: inline SmallObjectBase( void ) {} inline SmallObjectBase( const SmallObjectBase & ) {} inline SmallObjectBase & operator = ( const SmallObjectBase & ) { return *this; } inline ~SmallObjectBase() {} }; // end class SmallObjectBase
在上述实现过程中,作者Andrei又运用了一个C++编译器的技巧。在重载::operator delete(void *p, size_t size)时,实际上是需要SmallObject提供被析构对象的大小(运用了C++编译器删除对象前,即时产生一些代码计算被删除对象的大小),提供给SmallObject重载delete函数的“大小”参数来源于此。因此SmallObject提供了一个虚析构函数。因此,从SmallObject派生的任何类都会继承这个虚析构函数。
另一个问题就是对于整个程序而言,只需要一个唯一的SmallObjAllocator,因此SmallObject在包装第三层时,使用了Singleton模式。(Singleton不在本篇讨论范围内,在此不加描述)。
小结:
正在写测试程序,对比性能;待续。。。
相关推荐
这种策略在处理大量小对象分配时尤其有效,避免了频繁的内存碎片问题。 cpp-umalloc库提供的功能包括: 1. **高效分配**:通过预分配内存池,减少系统调用,提高内存分配速度。 2. **内存对齐**:确保分配的内存...
C#是一种面向对象的、类型安全的编程语言,由微软公司开发,广泛应用于Windows平台的软件开发。在创建小型文本编辑器时,C#的强大功能和丰富的类库使得开发过程更为便捷。例如,System.Windows.Forms库提供了各种...
在《精彩编程与编程技巧-用VB编写小型的网络系统》这篇文档中,主要介绍了如何利用Visual Basic (VB) 开发一个简单的小型网络系统。该系统的核心功能包括用户登录、注销、消息发送与接收等基本操作。通过分析文档...
【标题】:“小型超市管理系统UML大作业画的图”是指在进行信息技术课程或软件工程学习过程中,学生们可能被分配的一个项目任务,即设计并绘制一套用于小型超市管理的系统用例图、类图、序列图等UML(统一建模语言)...
书中的内容涵盖了多个关键主题,如Typelists、小型对象分配技术、泛化仿函数、单件模式、智能指针、对象工厂、抽象工厂、Visitor模式以及Multimethods等。以下是对这些知识点的详细阐述: 1. Typelists:Typelists...
在IT领域,特别是软件开发行业中,使用Visual Basic(简称VB)进行编程是一种常见的技能,尤其在构建小型网络系统时。本文将深入解析如何利用Visual Basic编写一个基础的网络系统,重点在于理解其核心功能和实现步骤...
在本实验中,我们主要探讨了C++编程语言中的面向对象程序设计的一些基本概念和技巧。以下是根据实验内容总结的相关知识点: 1. **开发环境与调试**:学习面向对象编程时,了解和熟悉开发环境至关重要,例如使用...
【面向对象程序设计实验】的目的是让学生深入了解C++的基础知识,包括开发环境的使用、调试技巧、标准输入输出函数的应用、const修饰符的理解与使用、内联函数的优缺点及适用场景、函数重载的概念、new和delete操作...
根据提供的标题、描述和部分内容,我们可以总结出一系列关于C++语言及其编程技巧的重要知识点。以下内容将详细探讨这些方面: ### C++语言简介 C++是一种面向对象的编程语言,由Bjarne Stroustrup在1979年基于...
14. **面向过程与面向对象**:虽然C语言本身是面向过程的,但可以通过结构体和指针模拟面向对象编程的一些概念,例如数据封装和继承。 15. **实践与项目经验**:理论知识与实际编程经验相结合是提高编程技巧的最好...
- **面向对象编程**:涵盖类与对象的概念,继承、多态、封装等OOP核心概念。 2. **进阶篇** - **异常处理**:讲解如何在代码中使用try-catch语句进行错误处理。 - **模板与泛型编程**:介绍模板的使用,包括函数...
1. **避免使用默认控件和文件名**:在创建 ASP.NET 项目时,Visual Studio .NET 会为新对象分配默认名称,如 WebForm1.aspx 和 Button1。这在小型示例项目中可能适用,但对于大型、复杂的应用程序,应为每个页面和...
此外,通过实际操作,学生将初步掌握局域网设计技巧,并提升独立开发网络应用的能力。 在设计方案论证阶段,设计思路分为五个部分:对象研究与需求调查、应用需求分析、网络拓扑结构与功能设计、技术设计原则以及...
2. **虚拟内存管理**:虚拟机需要模拟内存系统,包括分配、释放和访问内存。这通常涉及堆栈管理、内存地址映射等。在C++实现中,这可能通过内存管理类或结构体来完成。 3. **指令解释器**:虚拟机需要一个机制来...
超市管理系统是一种广泛应用的信息管理软件,它能够有效地帮助小型超市进行商品库存、销售、采购等日常业务的管理。本系统采用C++编程语言进行开发,具有操作简便、功能实用的特点,适用于对超市运营进行精细化管理...
7. **使用强类型和智能数据读取**:利用SqlCommand的ExecuteReader方法和NextResult,配合强类型对象,高效地读取和处理多个结果集,减少内存分配和资源消耗。 8. **仅获取必要的数据**:数据库查询应精确地获取所...
在这个C++课程设计中,学生将构建一个小型的商品销售管理系统,以此来实践C++编程语言的基础和高级特性,以及理解面向对象编程(OOP)的概念。这是一项综合性的任务,旨在提升学生的实际编程技能和问题解决能力。 ...