`
ld_hust
  • 浏览: 170284 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

小对象的分配技术

阅读更多

 

小对象分配技术是Loki提高效率的有效途径,Loki里广泛使用小对象, 如果使用系统默认分配器,因为薄记得缘故,可能代价在400%以上,所以这个是必须要解决的问题。我们首先来谈Chunks。

1.MemControlBlock结构

struct MemControlBlock 
{
    
bool    available_;
    MemControlBlock
*    prev_;
    MemControlBlock
*    next_;
}
;

是的,你看到了,这里没有块大小标志,这个时候注意这点会对你理解Chunk代码很有帮助,因为MemControlBlock里没有保存块大小的字段,所以它的上层——Chunk必须每次在需要的时候传入,而且必须自己保证不会修改传递数字的大小,这是非常基础的假设,宛如二者是一个类,是一个整体,你不能进行自己给自己错误数据的方式进行测试,那肯定是极端疯狂者的做法或者有特殊原因。

明白了这点,在Chunk的代码中关于size_t的意义就非常好理解了。

2.Chunks结构

struct Chunk 
{
    
void    Init(std::size_t    blockSize,unsigned char blocks);
    
void    Release();
    
void*    Allocate(std::size_t    blockSize);
    
void    Deallocate(void* p,std::size_t    blockSize);
    unsigned    
char*    pData_;
    unsigned    
char    firstAvailableBlock_;
    unsigned    
char    blockAvailable_;
}
;

我希望可以在声明上花多一点时间,这有助于你理解系统,从这个角度来说代码片段是没有任何意义的。

Init初始化本Chunk,它有若干MemControlBlock组成,你要指定块数量和块大小,特别要注意的是,如我们上边讨论的,一旦调用成功,你必须保证blockSize的大小每次都一样(永远不改变)。可以预期的是,在Init函数里使用了new方法开辟大块内存,以后每次Allocate都是回传特定一块。

对,还有一点要注意,基于Chunk的内存分配的块大小是在Init中已经确定的,是的,就是我反复强调的blockSize,你无法改变它,使用Allocate分派内存也是这个大小,而且每次一块,从这个层次上来说,这里的内存分配没有任何自由度,你可以认为这是new情况下的char,这是最小的分配单元。

有了以上两点注意,Allocate和Release的功能变的非常简单,取出一块特定大小的内存或归还一块内存。

pData_是Init中new出内存的位置。关于firstAvailableBlock_和blockAvailable_两个数据,是为了高效处理内存Allocate和Deallocate而定义的,使用它们,我们可以避免遍历查找内存块,尽管查找的效率好不错,但是不要忘了我们在做底层基础块,线性复杂度是我们十分不愿意看到的。

3.Chunk实现细节

为了高效查找、分配,我们在分配的时候对各个小块标记偏移位置,未分配的内存可以存放任何数据,所以在每个小块的开始存放偏移量是没有任何问题的,firstAvailableBlock记录第一个可用块,那查找该块的策略就是pData_ + blockSize × firstAvailableBlock,这是索引访问。唯一难理解的是归还内存。

我们知道,firstAvailableBlock指向第一块可用内存(如果有的话),归还的时候,它应该指向我们刚归还的那块,这很好处理firstAvailableBlock  × blockSize = pcur - pData_ ,关键是firstAvailableBlock在归还前指向可用内存如何在以后被检索到,是的,这块内存也应该有偏移量记录,我们可以记录该可用内存的索引,也就是那一刻的firstAvailableBlock了,问题变的异常简单。

具体代码不再贴了,核心三个函数的功能已经讲明白了。

我们使用了偏移量(构造索引查询)得方法避免了检索,这是Chunk效率的核心部分,Chunk又是小对象分配技术的核心部分。

 

 

2.大小一致得分配器

Chunk可以分配固定大小的有限数量内存块,为了达到分配任意多个内存块的目的,我们需要另外一个策略满足这个需求,也就是对Chunk的进一步包装。

小对象的分配技术将固定大小内存分配做成两个层次,当然,更多时候这种划分里考虑效率问题,但是你可以用心体会这里的隔离思想。

FixedAllocator的思想异常简单,为了应对可能的数量不固定的内存块分配,它使用vector存放Chunks,这已经可以解决问题,唯一需要注意的是如何提高效率。

我们记录可用Chunk位置,当有分配请求的时候直接获取,如果可用Chunk用完,则引发一次线性查找,如果还是未找到,那引发一次分配。为了提高哪怕是一点点的归还效率,我们记录最后分配内存的Chunk的位置,在归还的时候优先查询,如果未找到(也就是该块待归还内存不在当前Chunk里),会引发一次线性查找。

是的,FixedAllocator中,查找开始变的多了起来,但是似乎不太容易找到更好的方案了。

为了进一步提高效率,可以使用高速缓冲的策略,归还的内存留待下次使用,而不是直接归还,这是个不错的想法,但是暂时我不优化这个,我更想说的是Loki的小对象分配策略,确切的说是它使用的结构和有限的技术细节。

有必要提一下FixedAllocator的结构和分配方法:

class FixedAllocator
{
private:
    std::size_t        blockSize_;
    unsigned 
char    numBlocks_;
    typedef    std::vector
<Chunk>    Chunks;
    Chunks            chunks_;
    Chunks
*            allocChunk_;
    Chunks
*            deallocChunk_;
}
;

void*    FixedAllocator::Allocate()
{
    
if(allocChunk_ == 0 || allocChunk_->blockAvailable_ == 0)
    
{
        Chunks::iterator    i 
= chunks_.begin();
        
for (;;++i)
        
{
            
if (i == chunks_.end())
            
{
                chunks_.push_back(Chunk());
                Chunk
&    newChunk    = chunks_.back();
                newChunk.Init(blockSize_,numBlocks_);
                allocChunk_    
= &newChunk;
                deallocChunk_    
= &chunks_.front();
                
break;
            }

            
if (i->blocksAvailable_ > 0)
            
{
                allocChunk_    
= &*i;
                
break;
            }

        }

    }

    assert(allocChunk_ 
!= 0);
    assert(allocChunk_
->blocksAvailable_ > 0);
    
return    allocChunk_->Allocate(blockSize_);
}

 

 

3.SmallObjAllocator

如果第一次看到小型对象的分配细节,看完Chunk和FixedAllocator之后你一定很迷茫,在内存分配的起初两层,我们总是在研究固定大小的内存块,这似乎没有一点用处,大小一旦确定,你无法分配比blockSize大的内存块,即使是 blockSize得整数倍,因为一次只得到了一块内存,连续两次得到的内存块未必是连续的,你也不适合分配比blockSize小的内存块,这要浪费内存,而且很别扭。其实这都是因为Loki小对象分配器把Chunk和FixedAllocator做为原组使用了,真正分配任意大小内存的是SmallObjAllocator,看一下代码你就肯定明白了:

class SmallObjAllocator
{
public:
    SmallObjAllocator(std::size_t    chunkSize,std::size_t    maxObjectSize);
    
void*    Allocate(std::size_t    numBytes);
    
void    Deallocate(void*    p,std::size_t    size);
private:
    std::vector
<FixedAllocator>    pool_;
}
;

我们在构造函数里确定最大可分配的内存块大小,但是这并不意味着你只能分配不比maxObjectSize大的内存块,只是对于这样的内存块SmallObjAllocator转发给了系统的new。

我们已经知道,FixedAllocator只能分配固定大小的内存块,是的,到这里你明白了,pool_里有所有大小的Chunk,如此任意大小的块分配都可以转给合适的Chunk。在Deallocate里我们要求提供一个内存块大小的参数,避免pool_级别的遍历。

为了避免可能的内存浪费,pool_里的FixedAllocator并不是从1到maxObjectSize全部一次性分配的,一般情况下,一次性分配对内存的浪费是惊人的,相反,我们采用首次使用分配的策略。在首次使用的时候分配,依FixedAllocator里使用到的策略,我们对最后使用到的FixedAllocator记录以优化查询,最差情况下,SmallObjAllocator进行二分查找。

class SmallObjAllocator
{
public:
    ...
private:
    std::vector
<FixedAllocator>        pool_;
    FixedAllocator
*                    pLastAlloc_;
    FixedAllocator
*                    pLastDealloc_;
}
;

 

4.SmallObject

小对象分配策略在SmallObjAllocator层已经柳暗花明,可以猜测SmallObject是个顶级包装,使得使用更方便,确实是这样的,“SmallObject的定义 非常简单,只不过情节有点复杂”。

class SmallObject
{
public:
    
static    void*    operator new(std::size_t    size);
    
static    void    operator delete(void* p,std::size_t    size);
    
virtual    ~SmallObject(){}
}
;

需要注意的一点是delete的类级别重载:

class Base
{
public:
    
static void    operator delete(void* p,std::size_t    size)
    
{
        cout
<<"you call my delete"<<endl;
        ::
operator delete(p);
    }

}
;

int _tmain(int argc, _TCHAR* argv[])
{
    Base
*    haha    = new Base;
    delete    haha;
    
return 0;
}

如你所见,delete haha得时候,我们定义的delete得到了调用,也就是编译器提供给了我们额外的一个参数size,这也是Andrei要讨论编译期获取size的策略的原因,编译器给我们传了额外参数,我们有必要了解一下编译器开发者怎么做的。但注意,只是了解,我觉得在这里明白类operator delete以及定义类类虚析构函数的重要性就可以了。

对于整个程序里的SmallObject而言,我们只需要一个SmallObjAllocator,这就是Singleton模式了,可以抢先欣赏一下Loki得SingletonHolder:

typedef    SingletonHolder<SmallObjAllocator>    MyAlloc;
void*    SmallObject::operator new(std::size_t    size)
{
    
return    MyAlloc::Instance().Allocate(size);
}

void    SmallObject::operator delete(void* p,std::size_t size)
{
    MyAlloc::Instance().Deallocate(p,size);
}

分享到:
评论

相关推荐

    基于小型对象分配技术的GTNetS蠕虫仿真内存管理 (2012年)

    实验结果表明,应用小型对象分配技术后,系统在进行蠕虫仿真时的内存使用量得到显著降低,从而有助于提高仿真的规模和效率。 研究中还介绍了GTNetS的工作原理和特点,比如协议栈概念的引入,节点对象绑定多个网络...

    java内存对象分配过程研究

    ### Java内存对象分配过程研究 #### 一、引言 Java作为一门强大的面向对象编程语言,在实际开发过程中,对象的创建及其内存管理是至关重要的环节。深入理解对象在内存中的分配过程不仅能够帮助开发者设计出更为...

    C++ 设计新思维:范型编程与设计模式之应用

    2. 小型对象分配技术:针对小对象内存管理的优化技术,通常是为了减少内存碎片和提高内存利用率。例如,使用池分配器,将小对象集中分配在一个预先分配好的内存池中,以减少频繁的小块内存申请和释放带来的开销。 3...

    Moden C++ PartI

    小对象分配技术通常会使用池分配或位域技巧来优化内存的使用,以提高性能。 总的来说,《现代C++ PartI》提供了深入理解C++高级特性和最佳实践的机会,对于希望提升C++编程技能的开发者来说是一份宝贵的资源。通过...

    高效的,固定大小的对象池

    而`fixobjallocator.h`可能是专门用于对象分配的内存分配器的头文件,它可能负责实际的内存管理,如对齐、碎片控制等,以进一步优化性能。 在实际应用中,对象池常用于数据库连接、线程、网络套接字、图形渲染对象...

    suballoc:suballoc是C ++的一组类,可简化小对象的子分配-开源

    传统的内存分配方式,如new运算符,通常为每个对象分配一个完整的内存块,对于小对象而言,可能会造成大量内存碎片。而子分配则是在较大的内存块(通常称为“arena”或“bucket”)内进行小块内存的分配,这样可以...

    《面向对象技术与方法》07、类与对象.pdf

    1. **分配内存**:首先为新创建的对象分配内存。 2. **零初始化**:将分配的内存初始化为零值。 3. **执行构造器**:调用对象的构造方法来设置初始状态。构造方法会按照声明顺序依次执行父类和本类的构造方法。 4. *...

    MPEG-4视频对象分割技术

    通过为感兴趣的对象分配更多的比特,而对不重要的背景分配较少的比特,可以进一步提升编码效率。 #### 二、视频对象分割技术 ##### 2.1 视频对象的概念 在MPEG-4中,视频对象(Video Object, VO)指的是视频内容...

    深入理解java对象,包括对象创建和内存分配

    1. 分配内存:JVM在堆内存中为新对象分配空间。堆是Java中存储对象的主要区域,由垃圾收集器管理。 2. 初始化成员变量:所有实例变量被初始化为默认值,或者如果提供了初始化器,则使用指定的值。 3. 调用构造函数:...

    为成本对象标识输入内部作业分配.doc

    为成本对象标识输入内部作业分配.doc

    小块内存分配器设计与实现C++源代码程序小实例

    总结来说,小块内存分配器是提高C++程序内存管理效率的有效工具,尤其在处理大量小对象时。通过自定义的分配器类,我们可以控制内存的分配和释放,降低系统调用开销,减少碎片,从而提升程序运行速度。在深入理解和...

    Week15_第7讲_面向对象核心技术.pdf

    本讲义主要讨论了面向对象的核心技术,主要包括封装、类的继承以及方法的重写。 封装:在Java语言中,通过类(class)来实现封装,类可以定义私有成员变量(private)和公共方法(public)。私有成员变量只能在类的...

    面向对象技术的课件-西安交大内部资料

    - 行为型模式:策略、模板方法、观察者、迭代器、访问者、责任链、命令、备忘录等,关注对象间的交互和职责分配。 5. UML统一建模语言: - 类图:展示类、接口、关联、继承关系等。 - 用例图:描述系统参与者与...

    面向对象技术

    静态成员在类加载时就被初始化,而实例成员在创建对象时才分配内存。 9. **方法重载与重写**:方法重载(Overloading)是指在同一类中定义多个同名方法,但参数列表不同。方法重写(Overriding)是子类对父类已有的...

    面向对象技术(C++)-课件

    7. **虚析构函数**:如果基类包含动态分配的资源,需要确保在对象销毁时正确清理,此时应声明虚析构函数。 通过深入理解和实践这些C++的面向对象技术,不仅可以提升编程能力,还能更好地设计和维护复杂的软件系统。...

Global site tag (gtag.js) - Google Analytics