`
ld_hust
  • 浏览: 166724 次
  • 性别: 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的内存管理机制进行系统研究,提出小型对象内存分配技术。实验结果表明,小型对象分配技术可以有效减少系统的最大内存使用量,进而提高蠕虫仿真的规模和效率。

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

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

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

    本书包括Typelists,小型对象分配技术,泛化仿函数,(单件)实作技术, 智能指针,对象工厂,抽象工厂,Visitor(访问者,视察者), Multimethods等内容

    论文研究 - 人员分配的最优分配时间表

    这项研究是科学课程中作业安排的最佳分配。...因此,我们建议学校管理层在科学的人员-对象分配时间表中采用这些方法,管理和艺术学科以获得最佳利益。 因此,政府也应将这项研究的结果应用于其他学校。

    《面向对象技术》期末复习资料.doc

    • 非静态数据成员只有在定义了对象之后才存在(C++才会为它分配内存空间);静态数据成员不属于单个对象,即使没有定义它所属类的任何对象时,就已经存在了。 • 非静态数据成员的生存期局限在定义对象的块作用域内...

    对象的分配过程与垃圾回收过程(csdn)————程序.pdf

    对象的分配过程与垃圾回收过程(csdn)————程序

    软件开发技术考试复习题及参考答案

    C++有两种对象创建方式,静态内存分配方式和动态内存...new操作符为新对象分配内存并且返回它的一个指针,指针存放在指针变量中。new操作符可以包括构造函数中 的参数,如下例所示。 例:对象创建的动态内存分配方式

    深入Java核心 Java内存分配原理精讲

    Java内存分配与管理是Java的核心技术之一,今天我们深入Java核心,详细介绍一下Java在内存分配方面的知识。一般Java在内存分配时会涉及到以下区域:  ◆寄存器:我们在程序中无法控制  ◆栈:存放基本类型的...

    JAVA内存分配精讲.docx

    Java内存分配与管理是Java的核心技术之一,之前我们曾介绍过Java的内存管理与内存泄露以及Java垃圾回收方面的知识,今天我们再次深入Java核心,详细介绍一下Java在内存分配方面的知识。一般Java在内存分配时会涉及到...

    编译原理与技术第15讲 运行存储分配1

    即在编译时仅产生各种必要的信息,而在运行时刻,再动态地分配数据对象的存储空间栈式存储分配堆式存储分配运行存储分配策略静态和动态分别对应编译时刻和运行时刻运行时内

    C++ STL 开发技术导引(第6章)

    第1章 C++编程技术 2 1.1 类和对象 2 1.2 类的继承 5 1.3 函数重载 5 1.4 访问控制 7 1.5 操作符重载 8 1.6 显式类型转换 9 1.7 异常处理 13 1.8 名字空间 17 1.9 友员函数 20 1.10 内联函数 ...

    面向对象与UML资料

    面向对象技术产生的原因 19 面向对象方法的基本思想 19 概念 19 面向对象技术的特点 19 面向对象语言及系统 19 第二节 面向对象的分析 20 OOA分析的任务 20 OOA分析的原则 20 OOA分析过程 20 第三节 面向对象的设计 ...

    信息安全技术 网络安全等级保护测试评估技术指南.doc

    b) 检查对象包括安全策略、体系结构和要求、标准作业程序、系统安全计划和授权许可、系统互连的技术规范、事件响应计划,确保技术的准确性和完整性; c) 检查安全策略、体系结构和要求、标准作业程序、系统安全计划...

    C++ 面向对象教程 21 courses#

    1.5.3 对象建模技术(OMT) 19 1.6 为向OOP转变而采取的策略 19 1.6.1 逐步进入OOP 19 1.6.2 管理障碍 20 1.7 小结 21 第2章 数据抽象 22 2.1 声明与定义 22 2.2 一个袖珍C库 23 2.3 放在一起:项目创建工具 29 2.4 ...

    实业公司股权分配方案.doc

    三、分配对象 参与分配的人员范围: (1)公司中高层管理人员(包括公司总经理、副总经理、各部门正副职) (2)或对公司经营做出重大贡献的业务、技术骨干人员 注:虚拟股权的授予采取分期分批的方式,逐步覆盖上述...

    探索Python的内存模型和收集算法的演示代码_python代码_下载

    了解小型对象分配器如何以不同于您的直觉的方式处理大多数对象 了解 Python 的内存分配原语:blocks、pools和arenas 在 C 代码中找到负责 Python 内存行为的元素 通过实时代码探索查看引用计数 发现为什么单独的引用...

    基于内存映射文件的复杂对象快速读取方法_黄向平.pdf

    借助内存映射文件与自定义内存分配器,实现了结构复杂的 C++标准模板库容器对象跨进程无拷贝、无格式 转化的共享,有效降低了数据读取延时。通过性能的分析比较表明,与 NoSQL 内存数据库、嵌入式数据库比,读取性能...

    C++ STL开发技术导引(第5章)

    第1章 C++编程技术 2 1.1 类和对象 2 1.2 类的继承 5 1.3 函数重载 5 1.4 访问控制 7 1.5 操作符重载 8 1.6 显式类型转换 9 1.7 异常处理 13 1.8 名字空间 17 1.9 友员函数 20 1.10 内联函数 ...

    C++ STL开发技术导引(第3章)

    第1章 C++编程技术 2 1.1 类和对象 2 1.2 类的继承 5 1.3 函数重载 5 1.4 访问控制 7 1.5 操作符重载 8 1.6 显式类型转换 9 1.7 异常处理 13 1.8 名字空间 17 1.9 友员函数 20 1.10 内联函数 ...

Global site tag (gtag.js) - Google Analytics