深入理解Qt的隐式共享机制

20秒前 23阅读

在Qt中,一个关键的性能优化特性是其数据结构的隐式共享机制,这在Qt的文档和API中常被称为“隐式共享”或“写时复制(Copy-On-Write, COW)”。本文将详细介绍这一机制,并通过QString类的实现代码和相应的反汇编代码来阐释其工作原理。

深入理解Qt的隐式共享机制
(图片来源网络,侵删)

隐式共享的定义和优点

隐式共享是一种内存管理策略,它允许多个对象共享相同的数据副本,直到某个对象需要修改这些数据时才进行实际的数据复制。这种策略的优点包括:

  • 减少内存使用:多个对象可以共享同一个数据副本,而不是每个对象都持有一个数据副本。
  • 提高性能:减少数据复制的需要,从而降低程序的总体资源消耗。
  • 代码简化:开发者不需要关心数据如何被共享或复制,所有这些都由Qt框架自动管理。

    隐式共享在Qt中的实现

    在Qt中,很多基本数据类型,如QString、QList、QByteArray等,都实现了隐式共享。以下是QString类中赋值操作符的源码,可以直观了解隐式共享机制的实现:

    QString &QString::operator=(const QString &other) noexcept
    {
        other.d->ref.ref();
        if (!d->ref.deref())
            Data::deallocate(d);
        d = other.d;
        return *this;
    }
    

    代码解释

    1. other.d->ref.ref();

    增加源对象(other)数据的引用计数。
    因为在赋值后,目标对象和源对象都引用了相同的内存数据,所以对应的引用计数需要增加。

    1. if (!d->ref.deref()) Data::deallocate(d);

    通过deref()函数递减目标对象(d)的引用计数,并返回递减后是否仍有引用(如果还有其他引用,则返回true;如果此次操作后引用计数变为0,则返回false)。如果返回false,说明没有其他QString对象再引用这块数据了,那就需要调用Data::deallocate()来释放这块内存。
    因为目标对象在赋值后将指向新的内存数据,那么原来的数据引用计数需要递减,如果递减后变成0,那就说明没有对象在使用这块内存,需要释放,否则会导致内存泄漏。

    反汇编代码示例说明

    源码:

    QString sa = "hello";
    QString sb = sa;
    sb[0] = 'H';
    

    反汇编:

        QString sa = "hello";
    00007FF65A72461B  lea         rdx,[__xt_z+214h (07FF65A729CB4h)]  
    00007FF65A724622  lea         rcx,[sa]  
    00007FF65A724627  call        qword ptr [__imp_QString::QString (07FF65A72F170h)]  
    00007FF65A72462D  nop  
        QString sb = sa;
    00007FF65A72462E  lea         rdx,[sa]  
    00007FF65A724633  lea         rcx,[sb]  
    00007FF65A724638  call        qword ptr [__imp_QString::QString (07FF65A72F188h)]  
    00007FF65A72463E  nop  
        sb[0] = 'H';
    00007FF65A72463F  xor         r8d,r8d  
    00007FF65A724642  lea         rdx,[rsp+88h]  
    00007FF65A72464A  lea         rcx,[sb]  
    00007FF65A72464F  call        qword ptr [__imp_QString::operator[] (07FF65A72F178h)]  
    00007FF65A724655  mov         dl,48h  
    00007FF65A724657  mov         rcx,rax  
    00007FF65A72465A  call        qword ptr [__imp_QCharRef::operator= (07FF65A72F168h)]
    

    详细说明

    1. 在执行QString sb = sa;代码是,反汇编代码可以看出,先将sa和sb两个对象的地址加载到两个寄存器中,然后调用QString的拷贝构造函数。
    2. 在执行sb[0] = 'H';时,反汇编代码的最后一行代码,调用了QCharRef::operator=()赋值操作符,那么我们需要知道该赋值操作符具体做了什么:
    inline QCharRef &operator=(QChar c)
    {
        using namespace QtPrivate::DeprecatedRefClassBehavior;
        if (Q_UNLIKELY(i >= s.d->size)) {
    #ifdef QT_DEBUG
            warn(WarningType::OutOfRange, EmittingClass::QCharRef);
    #endif
            s.resize(i + 1, QLatin1Char(' '));
        } else {
    #ifdef QT_DEBUG
            if (Q_UNLIKELY(!s.isDetached()))
                warn(WarningType::DelayedDetach, EmittingClass::QCharRef);
    #endif
            s.detach();
        }
        s.d->data()[i] = c.unicode();
        return *this;
    }
    

    我们看到上面源码中if (Q_UNLIKELY(i >= s.d->size))判断下标有没有超出字符串大小,如果超出了,则需要调用resize重新开辟一块内存,如果没有超出,则调用detach()来分离数据,在分离的时候会判断当前引用计数,如果计数大于1,则会创建新的副本数据进行修改。

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]