C++ Primer 学习笔记_98_特殊的工具和技术 --优化内存分配

特殊的工具和技术

--优化内存分配

引言:

C++的内存分配是一种类型化操作:new为特定类型分配内存,并在新分配的内存中构造该类型的一个对象。new表达式自己主动执行合适的构造函数来初始化每一个动态分配的类类型对象。

new基于每一个对象分配内存的事实可能会对某些类强加不可接受的执行时开销,这种类可能须要使用用户级的类类型对象分配能够更快一些。

这种类使用的通用策略是,预先分配用于创建新对象的内存,须要时在预先分配的内存中构造每一个新对象

另外一些类希望按最小尺寸为自己的数据成员分配须要的内存。比如,标准库中的 vector类预先分配额外内存以保存添加的附加元素,将新元素添加到这个保留容量中。将元素保持在连续内存中的时候,预先分配的元素使vector能够高效地添加元素

在每种情况下(预先分配内存以保存用户级对象或者保存类的内部数据)都须要将内存分配对象构造分离开。将内存分配与对象构造分离开的明显的理由是,在预先分配的内存中构造对象非常浪费,可能会创建从不使用的对象。

当实际使用预先分配的对象的时候,被使用的对象必须又一次赋以新值。

更微妙的是,假设预先分配的内存必须被构造,某些类就不能使用它。比如,考虑vector,它使用了预先分配策略。假设必须构造预先分配的内存中的对象,就不能有基类型为没有默认构造函数的vector——vector没有办法知道如何构造这些对象。

【小心地雷】

本节提出的技术不保证使全部程序更快。即使它们确实能改善性能,也可能带来其它开销,如空间的使用或调试困难

最好将优化推迟到已知程序能够工作,而且执行时測试指出改进内存分配将解决已知的性能问题的时候。

一、C++中的内存分配

C++中,内存分配对象构紧密纠缠,就像对象和内存回收一样。

使用new 表达式的时候,分配内存,并在该内存中构造一个对象:使用delete表达式的时候,调用析构函数撤销对象,并将对象所用内存返还给系统

接管内存分配时,必须处理这两个任务。分配原始内存,必须在该内存中构造对象;释放内存之前,必须保证适当地撤销这些对象

【小心地雷】

未构造的内存中的对象进行赋值而不是初始化,其行为是没有定义的。对很多类而言,这样做引起执行时崩溃。赋值涉及删除现存对象,假设没有现存对象,赋值操作符中的动作就会有灾难性效果

C++提供下面两种方法分配和释放未构造的原始内存:

1)allocator,它提供可感知类型的内存分配。这个类支持一个抽象接口,以分配内存并随后使用该内存保存对象。

2)标准库中的operatornew 和 operatordelete,它们分配和释放须要大小的原始的、未类型化的内存。

C++还提供不同的方法在原始内存中构造撤销对象

1)allocator定义了名为construct和 destroy的成员,其操作正如它们的名字所指出的那样:construct成员在未构造内存初始化对象,destroy 成员在对象上执行适当的析构函数

2)定位new表达式接受指向未构造内存的指针,并在该空间中初始化一个对象或一个数组。

3)能够直接调用对象的析构函数来撤销对象。执行析构函数并不释放对象所在的内存

4)算法uninitialized_filluninitialized_copy像 fill和 copy 算法一样执行,除了它们的在目的地构造对象而不是给对象赋值之外。

二、allocator

allocator类是一个模板,它提供类型化的内存分配以及对象构造与撤销。


标准allocator类与定制算法


allocator<T>a;


定义名为a的allocator对象,能够用于分配内存或构造T类型的对象


a.allocate(n)


分配内存:分配原始的未构造内存以保存T类型的n个对象


a.deallocate(p,n)


释放内存:释放在名为p的T*指针中包括的地址处保存T类型的n个对象[原文Deallocatesmemory that held n objects of type T starting at addresscontained in the T* pointer named p]。

执行调用deallocate之前在该内存中构造的随意对象的destroy是用户的责任


a.construct(p,t)


在T*指针p所指内存中构造一个新元素。执行T类型的复制构造函数用t初始化该对象


a.destroy(p)


执行T*指针p所指对象的析构函数


uninitialized_copy(b,e,b2)


从迭代器b和e指出的输入范围将元素拷贝到从迭代器b2開始的未构造的原始内存中。该函数在目的地构造元素,而不是给它们赋值。假定由b2指出的目的地足以保存输入范围中元素的副本


uninitialized_fill(b,e,t)


将由迭代器b和e指出的范围中的对象初始化为t的副本。假定该范围是未构造的原始内存。使用复制构造函数构造对象


uninitialized_fill_n(b,e,t,n)


将由迭代器b和e指出的范围中至多n个对象初始化为t的副本。假定范围至少为n个元素大小。

使用复制构造函数构造对象

allocator类内存分配对象构造分开。当allocator对象分配内存的时候,它分配适当大小并排列成保存给定类型对象的空间。可是,它分配的内存是未构造的,allocator的用户必须分别construct和 destroy放置在该内存中的对象。

1、使用allocator管理类成员数据

回顾:vector类将元素保存在连续的存储中。为了获得可接受的性能,vector 预先分配比所需元素很多其它的元素。每次将元素加到容器中时。 vector成员检查是否有可用空间以容纳还有一元素。假设有,该成员在预分配内存中下一可用位置初始化一个对象;假设没有自由元素,就又一次分配vector: vector获取新的空间,将如今元素复制到空间,添加新元素,并释放旧空间。

vector所用存储開始是未构造内存,它还没有保存不论什么对象。将元素复制或添加到这个预分配空间的时候,必须使用allocator类的construct成员构造元素。

简易实现Vector类:

template <class T> class Vector
{
public:
    Vector():elements(0),first_free(0),end(0) {}
    void push_back(const T &);
    //...

private:
    static std::allocator<T> alloc;
    void reallocate();

    T *elements;
    T *first_free;
    T *end;
    //...
};

每一个Vector<T>类型定义一个allocator<T>类型的 static数据成员,以便在给定类型的Vector分配构造元素。每一个Vector对象在指定类型的内置数组中保存其元素,并维持该数组的下列三个指针,如图:

1)elements,指向数组的第一个元素

2)first_free,指向最后一个实际元素之后的那个元素

3)end,指向数组本身之后的那个元素

能够使用这些指针来确定Vector的大小和容量:

?Vector的size(实际使用的元素的数目)等于first_free- elements。

?Vector的capacity(在必须又一次分配Vector之前,能够定义的元素的总数)等于end- elements。

?自由空间(在须要又一次分配之前,能够添加的元素的数目)是end– first_free。

2、使用construct

push_back成员使用这些指针将新元素加到Vector末尾:

template <typename T>
void Vector<T>::push_back(const T &item)
{
    if (first_free == end)
    {
        reallocate();   //分配新空间并复制现存元素。将指针重置为指向新分配的空间
    }

    alloc.construct(first_free,item);
    ++ first_free;
}

一旦push_back函数知道还有空间容纳新元素,它就请求allocator对象构造一个新的最后元素。construct函数使用类型T的复制构造函数将item值拷贝到由first_free指出的元素。然后,将first_free加 1以指出又有一个元素在用。

3、又一次分配元素与复制元素

template <typename T>
void Vector<T>::reallocate()
{
    std::ptrdiff_t size = first_free - elements;
    std::ptrdiff_t newcapacity = 2 * max(static_cast<int>(size),1);

    T *newelements = alloc.allocate(newcapacity);
    uninitialized_copy(elements,first_free,newelements);

    for (T *p = first_free; p != elements; )
    {
        alloc.destroy(--p);
    }

    if (elements)
    {
        alloc.deallocate(elements,end - elements);
    }

    elements = newelements;
    first_free = newelements + size;
    end = elements + newcapacity;
}

我们使用一个简单但效果惊人的策略:每次又一次分配时分配两倍内存。

函数首先计算当前在用的元素数目,将该数目翻倍,并请求allocator对象来获得所需数量的空间。假设Vector为空,就分配两个元素。

uninitialized_copy调用使用标准copy算法的特殊版本号。

这个版本号希望目的地是原始的未构造内存,它在目的地复制构造每一个元素,而不是将输入范围的元素赋值给目的地,使用T的复制构造函数从输入范围将每一个元素拷贝到目的地。

for循环对旧数组中每一个对象调用allocator的 destroy成员它按逆序撤销元素,从数组中最后一个元素開始,以第一个元素结束。destroy函数执行 T类型的析构函数来释放旧元素所用的不论什么资源。

一旦复制和撤销了元素,就释放原来数组所用的空间。在调用deallocate 之前,必须检查elements是否实际指向一个数组

【注解】

deallocate期待指向由allocate分配的空间的指针,传给 deallocate一个零指针是不合法的。

最后,必须重置指针以指向新分配并初始化的数组。将first_free和 end 指针分别置为指向最后构造的元素之后的单元以及所分配空间末尾的下一单元。

//P636 习题18.1/2
//in Vector.h
#ifndef VECTOR_H_INCLUDED
#define VECTOR_H_INCLUDED

#include <algorithm>
#include <memory>

template <class T> class Vector
{
public:
    typedef T* iterator;

public:
    Vector():elements(0),first_free(0),end(0) {}
    void push_back(const T &);
    void reserve(const size_t );

    void resize(const size_t );
    void resize(const size_t ,const T &);

    T &operator[](const size_t);
    const T &operator[](const size_t) const;

    size_t size()
    {
        return first_free - elements;
    }
    size_t capacity()
    {
        return end - elements;
    }

    iterator begin()
    {
        return elements;
    }
    iterator last()
    {
        return first_free;
    }

private:
    static std::allocator<T> alloc;
    void reallocate();

    T *elements;
    T *first_free;
    T *end;
};

template <typename T> std::allocator<T> Vector<T>::alloc;

template <typename T>
void Vector<T>::reallocate()
{
    std::ptrdiff_t size = first_free - elements;
    std::ptrdiff_t newcapacity = 2 * std::max(static_cast<int>(size),1);

    T *newelements = alloc.allocate(newcapacity);
    std::uninitialized_copy(elements,first_free,newelements);

    for (T *p = first_free; p != elements; )
    {
        alloc.destroy(--p);
    }

    if (elements)
    {
        alloc.deallocate(elements,end - elements);
    }

    elements = newelements;
    first_free = newelements + size;
    end = elements + newcapacity;
}

#endif // VECTOR_H_INCLUDED

//in Vector.cpp
#include "Vector.h"

template <typename T>
void Vector<T>::push_back(const T &item)
{
    if (first_free == end)
    {
        reallocate();
    }

    alloc.construct(first_free,item);
    ++ first_free;
}

template <typename T>
void Vector<T>::reserve(const size_t capa)
{
    size_t size = first_free - elements;
    T *newelements = alloc.allocate(capa);

    if (size <= capa)
    {
        uninitialized_copy(elements,first_free,newelements);
    }
    else
    {
        uninitialized_copy(elements,elements + capa,newelements);
    }

    for (T *p = first_free; p != elements;)
    {
        alloc.destroy(--p);
    }
    if (elements)
    {
        alloc.deallocate(elements,end - elements);
    }

    elements = newelements;
    first_free = elements + std::min(size,capa);
    end = elements + capa;
}

template<class T>
void Vector<T>::resize(const size_t n)
{
    size_t size = first_free - elements;

    if (n > capacity())
    {
        reallocate();
        std::uninitialized_copy(elements + size,elements + n,T());
    }
    else if (n > size)
    {
        std::uninitialized_copy(elements + size,elements + n,T());
    }
    else
    {
        for (T *p = first_free; p != elements + n;)
        {
            alloc.destroy(--p);
        }
    }

    first_free = elements + n;
}

template<class T>
void Vector<T>::resize(const size_t n,const T &t)
{
    size_t size = first_free - elements;

    if (n > capacity())
    {
        reallocate();
        std::uninitialized_copy(elements + size,elements + n,t);
    }
    else if (n > size)
    {
        std::uninitialized_copy(elements + size,elements + n,t);
    }
    else
    {
        for (T *p = first_free; p != elements + n;)
        {
            alloc.destroy(--p);
        }
    }

    first_free = elements + n;
}

template<class T>
T &Vector<T>::operator[](const size_t index)
{
    return elements[index];
}
template<class T>
const T &Vector<T>::operator[](const size_t index) const
{
    return elements[index];
}

版权声明:本文博客原创文章。博客,未经同意,不得转载。

时间: 2024-08-04 20:24:45

C++ Primer 学习笔记_98_特殊的工具和技术 --优化内存分配的相关文章

C++ Primer 学习笔记_98_非一般工具与技术 -优化内存分配

特殊工具与技术 --优化内存分配 引言: C++的内存分配是一种类型化操作:new为特定类型分配内存,并在新分配的内存中构造该类型的一个对象.new表达式自动运行合适的构造函数来初始化每个动态分配的类类型对象. new基于每个对象分配内存的事实可能会对某些类强加不可接受的运行时开销,这样的类可能需要使用用户级的类类型对象分配能够更快一些.这样的类使用的通用策略是,预先分配用于创建新对象的内存,需要时在预先分配的内存中构造每个新对象. 另外一些类希望按最小尺寸为自己的数据成员分配需要的内存.例如,

C++ Primer 学习笔记_98_特殊工具与技术 --优化内存分配

特殊工具与技术 --优化内存分配 引言: C++的内存分配是一种类型化操作:new为特定类型分配内存,并在新分配的内存中构造该类型的一个对象.new表达式自动运行合适的构造函数来初始化每个动态分配的类类型对象. new基于每个对象分配内存的事实可能会对某些类强加不可接受的运行时开销,这样的类可能需要使用用户级的类类型对象分配能够更快一些.这样的类使用的通用策略是,预先分配用于创建新对象的内存,需要时在预先分配的内存中构造每个新对象. 另外一些类希望按最小尺寸为自己的数据成员分配需要的内存.例如,

C++ Primer 学习笔记_99_特殊工具与技术 --优化内存分配[续1]

特殊工具与技术 --优化内存分配[续1] 三.operator new函数和operator delete 函数 – 分配但不初始化内存 首先,需要对new和delete表达式怎样工作有更多的理解.当使用new表达式 string *sp = new string("initialized"); 的时候,实际上发生三个步骤: 1)首先,表达式调用名为operator new 的标准库函数,分配足够大的原始的未类型化的内存,以保存指定类型的一个对象; 2)接下来,运行该类型的一个构造函数

C++ Primer 学习笔记_100_特殊工具与技术 --优化内存分配[续2]

特殊工具与技术 --优化内存分配[续2] 七.一个内存分配器基类 预先分配一块原始内存来保存未构造的对象,创建新元素的时候,可以在一个预先分配的对象中构造:释放元素的时候,将它们放回预先分配对象的块中,而不是将内存实际返还给系统.这种策略常被称为维持一个自由列表.可以将自由列表实现为已分配但未构造的对象的链表. 我们将定义一个名为 CachedObj 的新类来处理自由列表.像 QueueItem 这样希望优化其对象分配的类可以使用 CachedObj 类,而不用直接实现自己的 new 和 del

C++ Primer 学习笔记_89_用于大型程序的工具 --异常处理[续2]

用于大型程序的工具 --异常处理[续2] 八.自动资源释放 考虑下面函数: void f() { vector<string> v; string s; while (cin >> s) { v.push_back(s); } string *p = new string[v.size()]; //... delete p; } 在正常情况下,数组和vector都在退出函数之前被撤销,函数中最后一个语句释放数组,在函数结束时自动撤销vector. 但是,如果在函数内部发生异常,则将

C++ Primer 学习笔记_90_用于大型程序的工具 --异常处理[续3]

用于大型程序的工具 --异常处理[续3] 九.auto_ptr类[接上] 5.auto_ptr对象的复制和赋值是破坏性操作 auto_ptr和内置指针对待复制和赋值有非常关键的区别.当复制auto_ptr对象或者将它的值赋给其他auto_ptr对象的时候,将基础对象的所有权从原来的auto_ptr对象转给副本,原来的auto_ptr对象重置为未绑定状态. auto_ptr<string> strPtr1(new string("HELLO!")); auto_ptr<

C++ Primer 学习笔记_93_用于大型程序的工具 --命名空间[续2]

用于大型程序的工具 --命名空间[续2] 五.类.命名空间和作用域 名字的可见性穿过任意嵌套作用域,直到引入名字的块的末尾. 对命名空间内部使用的名字的查找遵循常规C++查找规则:当查找名字的时候,通过外围作用域外查找.对命名空间内部使用的名字而言,外围作用域可能是一个或多个嵌套的命名空间,最终以全包围的全局命名空间结束.只考虑已经在使用点之前声明的名字,而该使用仍在开放的块中: namespace A { int i; namespace B { int i; int j; int f1()

C++ Primer 学习笔记_94_用于大型程序的工具 --命名空间[续3]

用于大型程序的工具 --命名空间[续3] 六.重载与命名空间 正如我们所见,每个命名空间维持自己的作用域,因此,作为两个不同命名空间的成员的函数不能互相重载.但是,给定命名空间可以包含一组重载函数成员. 1.候选函数与命名空间 命名空间对函数匹配有两个影响.一个影响是明显的:using声明或using 指示可以将函数加到候选集合.另一个影响则微妙得多. 正如前节所见,有一个或多个类类型形参的函数的名字查找包括定义每个形参类型的命名空间.这个规则还影响怎样确定候选集合,为找候选函数而查找定义形参类

C++ Primer 学习笔记_95_用于大型程序的工具 --多重继承与虚继承

用于大型程序的工具 --多重继承与虚继承 引言: 大多数应用程序使用单个基类的公用继承,但是,在某些情况下,单继承是不够用的,因为可能无法为问题域建模,或者会对模型带来不必要的复杂性. 在这些情况下,多重继承可以更直接地为应用程序建模.多重继承是从多于一个直接基类派生类的能力,多重继承的派生类继承其所有父类的属性. 一.多重继承 1.定义多个类 为了支持多重继承,扩充派生列表: class Bear : public ZooAnimal { //... }; 以支持由逗号分隔的基类列表: cla