C++写时拷贝

一、什么是写时拷贝

写入时复制是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变。


如果后续没有对资源进行修改的操作,甚至不会进行数据拷贝,如果在fork函数返回之后,马上调用exec()函数,也不会有数据拷贝

exec函数的作用是让子进程执行其它程序,即替换当前进程的映像

 
 
此作法的主要优点是如果调用者没有修改该资源,就不会有副本被建立,因此多个调用者只是读取操作是可以共享同一份资源。
 
写时拷贝技术是一种很重要的优化手段,核心是懒惰处理实体资源请求,在多个实体资源之间只是共享资源,起初是并不真正实现资源拷贝,
 
 
 

 

 

 
只有当实体有需要对资源进行修改时才真正为实体分配私有资源。但写时拷贝技术技术也有它的优点和缺点
 
1、写时拷贝技术可以减少分配和复制大量资源时带来的瞬间延时,但实际上是将这种延时附加到了后续的操作之中。
 
2、写时拷贝技术可以减少不必要的资源分配。比如fork进程时,并不是所有的页面都需要复制,父进程的代码段和只读数据段都不被允许修改,所以无需复制。
 
 
二、写时拷贝技术的两种实现方案
 
当一个内存空间被多个对象引用的时候,如果这些对象没有对内存空间进行修改,则不进行内存的拷贝,有对象对内存空间进行修改的时候,检查是否有除自己外别的对象使用这块空间,若有,则自己重新开辟空间进行更改,不影响其他对象;若没有其他对象使用此空间,则说明只有自己使用此空间,就进行更改。此时引入引用计数,用于统计有多少对象在使用此空间。
 
写时复制两种实现方案的区别在于,引用计数是否要开辟一个新的空间去保存,还是保存在拷贝地址的首部(数据首地址往后偏移4个字节)

 

方案一:

如下图所示:

 

 

当创建一个对象s1,再拷贝构造一个对象s2时,引用计数_refCount自动加1,此时为2。若此时s2要对自身进行修改,则s2重新开辟一块空间,检查引用计数_refCount是否大于1,若大于1,,则进行修改,这样就不会影响其他对象。

 

 

s2重新开辟一块儿空间,再修改s1原来空间的引用计数_refCount,使其减1,重新开辟的空间中因为此时只有s2一个对象,所以引用计数_refCount也为1.

 

小结:此方案的写时拷贝计数是同时开辟两块空间,一个自身存放的内容,一个存放引用计数_refCount,同时管理两块空间,统计当前使用此空间的对象数,当要修改当前空间的时候,进行对引用计数的判断,再决定是否开辟新的空间。

代码如下:

class String
{
public:
    //构造函数
    String(char* str="")
        :_str(new char[strlen(str)+1])
        ,_refCount(new int(1))
    {
        strcpy(_str, str);
    }
 
 
    //拷贝构造函数
    String(String& s)
    {
        _str = s._str;
        _refCount = s._refCount;
        ++(*_refCount);
    }
 
 
    String& operator=(const String& s)
    {
        if (_str != s._str)
        {
            Release();
 
            _str = s._str;
            _refCount = s._refCount;
            (*_refCount)++;
        }
        return *this;
    }
 
 
    void Release()
    {
        if (--(*_refCount) == 0)
        {
            cout << "delete" << endl;
            delete[] _str;
            delete _refCount;
        }
    }
 
    ~String()
    {
        Release();
    }
 
    void CopyOnWrite()
    {
        if (*_refCount > 1)
        {
            char* tmp = new char[strlen(_str) + 1];
            strcpy(tmp, _str);
            --(*_refCount);
 
            _str = tmp;
            _refCount = new int(1);
        }
    }
 
    char& operator[](size_t pos)
    {
        CopyOnWrite();
        return _str[pos];
    }
 
    char operator[](size_t pos) const
    {
        return _str[pos];
    }
private:
    char* _str;
    int* _refCount;
};
View Code

方案1中是开辟了两块空间进行管理,方案2采用开辟一块空间进行写时拷贝的操作。

开辟一块空间,在这块空间的头4个字节中放置引用计数,真正存放内容的空间从第5个字节开始。一次性多开辟4个字节进行写时拷贝的操作。

具体如下图所示:

 

 当进行操作时,先检查引用计数的个数,然后进行判断是否开辟新的空间,同时修改引用计数的值,防止空间不能释放。

具体例子如下:
当创建了3个对象,s1,s2,s3同时指向一个空间,此时引用计数为3,再创建1个对象s4,s4的引用计数为1。

 

 再进行操作s3 = s4;此时对应的引用计数和对象的指向都需要更改,更改之后如下图所示:

 

 

 对象s3指向了s4,同时s3原来的空间的引用计数进行减1,新指向空间的引用计数进行加1.

 

小结:方案2的写时拷贝计数使用一块空间进行内容和引用计数的管理和操作,不开辟两块空间,方便管理。

代码如下:

class String
{
public:
    String(const char* str)
        :_str(new char[strlen(str) + 5])
    {
        _str += 4;
        strcpy(_str, str);
        GetRefCount();
    }
 
 
    String(const String& s)
        :_str(s._str)
    {
        ++GetRefCount();
    }
 
    ~String()
    {
        if (--GetRefCount() == 0)
        {
            cout << "delete" << endl;
            delete[](_str - 4);
        }
    }
 
    String& operator=(const String& s)
    {
        if (_str != s._str)
        {
            if (--GetRefCount() == 0)
            {
                delete[](_str - 4);
            }
 
            _str = s._str;
            GetRefCount()++;
        }
        return *this;
    }
 
    int& GetRefCount()
    {
        return *((int*)(_str - 4));
    }
 
    char& operator[](size_t pos)
    {
        CopyOnWrite();
        return _str[pos];
    }
 
    void CopyOnWrite()
    {
        if (GetRefCount()>1)//当一块空间有两个或者两个以上的对象指向时,才写时拷贝
        {
            char* tmp = new char[strlen(_str) + 5];
            strcpy(tmp, _str);
            --GetRefCount();
            _str = tmp;
            GetRefCount() = 1;
        }
    }
 
private:
    char* _str;
    int* _refCount;
};
View Code

 

 以上转载自https://blog.csdn.net/zy20150613/article/details/76021541

 
三、写时拷贝技术的应用
 
1、虚拟内存管理中的写时复制
 
一般把这种被共享访问的页面标记为只读。当一个task试图向内存中写入数据时,内存管理单元(MMU)抛出一个异常,内核处理该异常时为该task分配一份物理内存并复制数据到此内存,
 
重新向MMU发出执行该task的写操作。
 
   比如Linux的fork()使用写时拷贝页来实现新进程的创建,它是一种可推迟甚至避免数据拷贝的技术,开始时内核并不会复制整个地址空间,而是让父子进程共享地址空间,只有在写时才复制地址空间,使得父子进程都拥有独立的地址空间,即资源的复制是在只有需要写入时才会发生。在此之前都是以读的方式去和父进程共享资源,这样,在页根本不会被写入的场景下,fork()立即执行exec(),无需对地址空间进行复制,fork()的实际开销就是复制父进程的一个页表和为子进程创建一个进程描述符,也就是说只有当进程空间中各段的内存内容发生变化时,父进程才将其内容复制一份传给子进程,大大提高了效率。
 
 

2、数据存储中的写时复制

  Linux等的文件管理系统使用了写时复制策略。

  举个例子,比如我们有个程序要写文件,不断地根据网络传来的数据写,如果每一次fwrite或是fprintf都要进行一个磁盘的I/O操作的话,都简直就是性能上巨大的损失,

  因此通常的做法是,每次写文件操作都写在特定大小的一块内存中(磁盘缓存),只有当我们关闭文件时,才写到磁盘上(这就是为什么如果文件不关闭,所写的东西会丢失的原因)




3、软件应用中的写时复制

在我们经常使用的STL标准模板库中的string类,也是一个具有写时才拷贝技术的类。为了提高性能,STL中的许多类都采用了写时拷贝技术。但是在C++11标准中为了提高并行性取消了这一策略

 

class String
{
public:
    //构造函数(分存内存)
    String(char* tmp)
    {
        _Len = strlen(tmp);
        _Ptr = new char[_Len + 1 + 1];
        strcpy(_Ptr, tmp);
        // 在数组尾部设置引用计数
        _Ptr[_Len + 1] = 0; 
    }
    //析构函数
    ~String()
    {
        //引用计数减一
        _Ptr[_Len + 1]--; 
       // 引用计数为0时,释放内存
        if (_Ptr[_Len + 1] == 0)
        {
            delete[] _Ptr;
        }
    }

    //拷贝构造(共享内存)
    String(string& str)
    {
        if (this->_Ptr != str)
        {
            //共享内存,.data()返回的是将string的类型转换成char类型的指针
            const char *p = str.c_str();
            char* pp;
            strcmp(pp, p);
            this->_Ptr = pp; 
            this->_Len = str.size();
            this->_Ptr[_Len + 1] ++; //引用计数加一

        }
    }

    //对[]符进行重载,对字符串进行操作的时候,开始写时复制
    char& operator[](unsigned int idx)
    {
        if (idx > _Len || _Ptr == 0)
        {
            static char nullchar = 0;
            return nullchar;
        }
        //引用计数减一
        _Ptr[_Len + 1]--; 

        char* tmp = new char[_Len + 1 + 1];

        strncpy(tmp, _Ptr, _Len + 1);

        _Ptr = tmp;
        // 设置新的共享内存的引用计数
        _Ptr[_Len + 1] = 0; 
        return _Ptr[idx];
    }

private:
    int _Len;
    char* _Ptr;
};

 

 

posted @ 2020-06-01 18:14  知道了呀~  阅读(1123)  评论(0编辑  收藏  举报