什么是隐式共享类?

7

我过去6个月一直在使用Qt,但仍然难以理解隐式共享类的概念。我有以下问题:

  1. 什么是隐式共享类?它们如何工作?
  2. Trolltech Qt网站上说它最大化资源使用和最小化复制,请解释这是如何实现的。
  3. 有没有人能给出任何例子来更好地理解?欢迎提供任何有或无示例解释此概念的网站链接。

感谢大家的回答。关于这个主题,我想到了另一个点,即栈对象指向堆分配的共享数据。这是图表:this image..

对此有何看法?什么是引用计数?当对象引用共享数据时,它是否会增加计数器?反之亦然?

3个回答

14

想象一下以下情境。你使用的是C++03,你写了如下代码:

string a("hello");
string b = a; 
此时,您有两个字符串对象a和b,每个对象都有自己的缓冲区来存储字符数组。尽管缓冲区的内容完全相同,但是a和b仍然拥有各自的"hello"副本。这样会浪费内存。如果它们共用一个缓冲区,您只需要使用一个字符数组来为两个字符串存储"hello world"。
现在对于QString,情况略有不同:
QString a("Hello");
QString b = a;
在这种情况下,只有a创建了一个char数组来存储“hello”。而b没有创建自己的char数组,它只是指向a的char数组。这样可以节省内存。
现在,如果您执行b [0] ='M',即修改b,那么b将创建自己的char数组,复制a的数组内容,然后修改自己的数组。
在Java中,字符串是不可变对象。换句话说,Java没有为String类提供任何方法来修改其内容。这样做是为了始终可以共享这种数据。
补充一些其他人提到的内容: 我如何知道我可以释放char数组?
这就是“引用计数”的作用。当创建一个对象并将其设置为指向char数组时,其引用计数将增加1,因此它知道仍在使用它的对象数量。当指向它的对象被销毁时,引用计数会减少。当计数器达到零时,char数组知道没有人再使用它,因此可以释放。
这是一个非常粗糙的引用计数实现。我无意准确或正确地实现C++中的拷贝构造函数和赋值运算符。我没有检查实现是否有效。认为这是类似C ++的算法描述。我只是想教授概念。但是想象一下你有这些类:
class SharedData{
  private:
    int refcount;
    int data;

  public:
    SharedData(int _data){data=_data;refcount=1;}
    void incRef(){refcount++;}
    void decRef(){--refcount; if(refCount==0) delete this;}
};

class Data{
       SharedData* shared;
    public:
        Data(int i){shared = new Data(i);}
        Data(const Data& data){shared = data.shared; shared->incRef();}
        const Data& operator=(const Data& data){if(shared!=data.shared){
                                       shared->decRef();
                                       shared = data.shared;
                                       shared->incRef();}
        }
        ~Data(){shared->decRef();}
};

两个 Data 类的对象可以共享同一个 SharedData 对象,因此:

void someFunction() {
    Data a(3) //Creates a SharedData instance and set refcount to 1
    if (expression) {
       Data b = a; //b points to the same SharedData than a. refcount is 2
       b = Data(4);// b points to  diferent SharedData. refcount of SharedData of a is decremented to 1 and b's SharedData has refcount 1
       //destructor of b is called. Because shared data of b has now refcount == 0, the sharedData is freed;
    }
    //destructor of a is called, refcount is decremented again
    // because it is zero SharedData is freed
}

资源使用被最大化,拷贝被最小化。 ab 都使用了相同的SharedData(也称为 int 3)。不是将a中的4复制到b中,而是它们共享相同的数据。一个 int 不是什么大不了的事情,但想象一下如果 SharedData 是一个大字符串或任何其他更复杂的数据结构。仅复制一个指针比几十个字节要快得多。当您不需要副本时,这也可以节省大量内存。

什么是写时复制?
回想一下上面我们做 b[0]='M' 时说的话。那就是写时复制ba 共享同一个字符数组。但是 b 需要修改字符串,它不能直接修改,因为这会同时改变 a 中的字符串。所以 b 必须创建自己的 char 数组的副本才能对其进行更改。由于只有在修改数组时才必须创建副本,因此称为写时复制


std::string 的实现通常比这要复杂得多,包括引用计数、COW,有时甚至还包括短字符串优化(将长度较短的字符串内联)。 - Marcelo Cantos
COW 在 std::string 中是明确禁止的,理由充分。当涉及到多线程时,它会变成一个巨大的性能下降。 - Puppy
确切地说,我没有使用std::命名空间前缀,因为这个原因。我提到了C++03,因为C++11有移动构造函数。所以请认为我上面提到的字符串是一个非常简单的类,它具有复制构造函数和赋值运算符,总是创建一个缓冲区并进行复制。 - André Oriani
我认为我关于 b=Data(4) 的说法不准确。Data(4) 创建了一个 data==4SharedData。对 b 的赋值将引用计数增加到2。在 if 结束时,bData(4) 的析构函数都被调用。因此,引用计数被设置为零。 - André Oriani

2
根据我阅读的http://doc.qt.io/archives/qt-4.7/implicit-sharing.html...
它基本上只是一个通用名称,用于任何使用引用计数和写时复制来避免不必要拷贝由类管理的数据的类。
引用计数
引用计数是一种技术,确保只要有人对对象感兴趣,它就会存在。任何想要暂时持有该对象的代码都会增加引用计数。当它失去对该对象的兴趣时,它会减少引用计数,并且如果引用计数为零,表示它是最后一个感兴趣的方,那么它也会销毁该对象。
在Qt共享类的情况下,引用计数似乎完全自动化。引用计数通过相关类的构造函数和析构函数进行管理。
写时复制
除了通过引用计数进行共享外,类还可以通过在修改底层数据之前复制它来确保不同方不会破坏彼此的对象版本。这称为写时复制或COW惯用语。

0

(免责声明:我从未使用过Qt,因此在某些细节上可能会有所错误-欢迎评论和改进...)

官方文档写得相当好。从中我推断出隐式共享类是那些实例不会实际复制其基础数据的类(这将是一项CPU和内存密集型操作),而是仅保留对其的引用(即存在一个共同的支持引用计数的数据对象)。这种优化使对象使用更少的内存和CPU时间,而对于它们的环境来说,似乎每个对象都有自己独立的数据。当然,如果对象是可变的,则必须执行实际的字节到字节数据复制,并且这些类会自动实现此机制(setter方法使用detach()方法使对象独立于公共数据并创建自己的实际副本)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接