Qt/C++ QVariant内容的零拷贝访问(获取与修改)

6 篇文章 1 订阅

1. 简介

Qt/C++ 借助QVariant实现可存储通用类型的容器这篇文章中,我们介绍了如何使用QVariant来实现一个可存储通用数据类型的容器。其中,最重要的就是QVariant类。它的作用类似于Qt数据类型的联合,它还能支持存储用户自定义的类型。

文章中提到,toT()(例如toInt()toString())是const方法(value()也是如此),这意味着通过这些方法获取的数据实际上是QVariant内部数据的副本,即外部对获取得到的数据的修改并不会影响其内部的数据。但是,在实际开发中,我们常常希望获取的是QVariant内部数据本身,尤其是当QVariant内存储的是不支持隐式共享的大批量数据时,因为这意味着这里会发生一次拷贝,影响程序性能。除了出于性能的考虑,我们可能还需要对内部的数据进行修改。也就是说,我们对QVariant的内部数据的访问提出了如下两个需求:

  1. 零拷贝读取内部数据;
  2. 直接对内部数据完成读-改-写。

下面就介绍如何使用一些奇技淫巧来实现这两个需求。

2. 零拷贝读取内部数据

如果想要使用零拷贝的方式访问数据内容,我们首先能想到的就是使用指针。不得不说,Qt的设计者们真的是为我们预留了很大的自由度,我们确实可以通过指针直接访问QVariant内存储的数据。QVariant(Qt 5.15)提供了如下三个接口用于获取内部数据指针:

void *data();
const void *constData() const;
inline const void *data() const { return constData(); }

注意,官方文档对void *data()的描述如下:

Returns a pointer to the contained object as a generic void* that can be written to.
This function detaches the QVariant. When called on a null-QVariant, the QVariant will not be null after the call.

也就是说,void *data()会在内部调用detach(),这意味着这里可能会导致一次数据拷贝(QVariant处于共享状态时)。因此,如果仅仅是为了读取数据,建议选用constData()。后面会通过实验验证。

既然数据指针都获取到了,要访问内容不就是轻而易举的事情嘛。为了让这件事情更加“泰裤辣”,我们可以使用如下的函数模板实现更加通用的转换:

template<typename T>
inline const T *castConstPtr(QVariant & var)
{
    if (var.canConvert<T>()) {
        return reinterpret_cast<const T *>(var.constData());
    }
    return nullptr;
}

template<typename T>
inline T *castPtr(QVariant & var)
{
    if (var.canConvert<T>()) {
        return reinterpret_cast<T *>(var.data());
    }
    return nullptr;
}

下面我们用实验来验证一下data()constData()的效果以及它们的区别。我的验证环境如下:

  • CPU: Intel® Core™ i7-10750H CPU @ 2.60GHz 2.59 GHz
  • OS:Windows 11
  • Compiler:MSVC 19.32.31332.

我们分别使用std::vector<uint8_t>QVector<uint8_t>创建包含10亿个元素(每个元素的值设置为66)的实例,它们占用内存大小分别约等于953.74MB。我们先将数据放入QVariant,然后分别使用上面的castConstPtrcastPtr()以及原生的value()获取数据,并通过QElapsedTimer统计各个操作的耗时。为了解释的便利,本文设计了两个实验。

2.1. 实验1:感受castConstPtr和castPtr的魅力

实验1的关键代码如下:

QElapsedTimer profiler;
QVariant var;
QVariant var1;
std::vector<uint8_t> vec(1000000000, 66);
QVector<uint8_t> vec1(1000000000, 66);
profiler.start();
var.setValue(vec);
auto consume1 = profiler.restart();
var1.setValue(vec1);
auto consume2 = profiler.restart();
const auto *constPtr = castConstPtr<std::vector<uint8_t>>(var);
auto consume3 = profiler.restart();
const auto *constPtr1 = castConstPtr<QVector<uint8_t>>(var1);
auto consume4 = profiler.restart();
const auto *ptr = castPtr<std::vector<uint8_t>>(var);
auto consume5 = profiler.restart();
const auto *ptr1 = castPtr<QVector<uint8_t>>(var1);
auto consume6 = profiler.restart();
auto value = var.value<std::vector<uint8_t>>();
auto consume7 = profiler.restart();
auto value1 = var1.value<QVector<uint8_t>>();
auto consume8 = profiler.restart();
if (constPtr != nullptr && constPtr1 != nullptr &&
    ptr != nullptr && ptr1 != nullptr) {
    qInfo() << "constPtr[0]" << constPtr->at(0);
    qInfo() << "constPtr1[0]" << constPtr1->at(0);
    qInfo() << "ptr[0]" << ptr->at(0);
    qInfo() << "ptr1[0]" << ptr1->at(0);
    qInfo() << "value[0]" << value.at(0);
    qInfo() << "value1[0]" << value1.at(0);
}
else {
    qCritical() << "cast error!";
}
qInfo() << "var detach" << var.isDetached();
qInfo() << "var1 detach" << var1.isDetached();
qInfo() << "[setValue] for var consume" << consume1 << "ms";
qInfo() << "[setValue] for var1 consume" << consume2 << "ms";
qInfo() << "[castConstPtr] for var consume" << consume3 << "ms";
qInfo() << "[castConstPtr] for var1 consume" << consume4 << "ms";
qInfo() << "[castPtr] for var consume" << consume5 << "ms";
qInfo() << "[castPtr] for var1 consume" << consume6 << "ms";
qInfo() << "[value] for var consume" << consume7 << "ms";
qInfo() << "[value] for var1 consume" << consume8 << "ms";

程序运行输出的结果如下:

constPtr[0] 66
constPtr1[0] 66
ptr[0] 66
ptr1[0] 66
value[0] 66
value1[0] 66
var detach true
var1 detach true
[setValue] for var consume 212 ms
[setValue] for var1 consume 0 ms
[castConstPtr] for var consume 0 ms
[castConstPtr] for var1 consume 0 ms
[castPtr] for var consume 0 ms
[castPtr] for var1 consume 0 ms
[value] for var consume 211 ms
[value] for var1 consume 0 ms

首先,根据前6行的输出,我们可以得知通过castConstPtr()castPtr()得到的数据是正确的。

查看第7-8行的输出,可以看到varvar1都已经处于detached状态。

查看9-10行,我们发现,将QVector<uint8_t>存入QVariant中的速度比std::vector<uint8_t>快太多了。这就是Qt隐式共享的威力所在,因为QVector支持隐式拷贝,实际上拷贝到QVariant仅仅是QVector的元数据,而不是真实的数据内容。

查看11-14行可知,castConstPtr()castPtr()的访问速度竟然是一样快的。因为在7-8行中,我们得知了varvar1都已经被detach(),那是不是就意味着进行了拷贝呢?答案是否定的。下面就仔细看下这其中发生了什么:我们先查看isDetached()的定义:

inline bool QVariant::isDetached() const
{ return !d.is_shared || d.data.shared->ref.loadRelaxed() == 1; }

通过简单的验证便可以知道,在执行data()之前,varis_shared是1且shared->ref.loadRelaxed()为1;对于var1is_shared是0,shared->ref.loadRelaxed()是1000000000(原因未深究)。因此,它们在data()之前就已经处于detached的状态了,故而在执行data()时,两者都没有发生拷贝。其实这个结果也是符合常理的,因为varvar1各自都只有一个备份,此时不存在共享数据的情况,所以按常理来说也不应该发生拷贝。但是,如果var或者var1处于共享状态时,那结果就不一样了,详情参看实验2。

从最后的两行日志可以看出两点:

  1. value()返回数据时确实会发生一次拷贝;
  2. Qt的隐式共享功能真的很香。因为对于这类数据结构而言,即使我们不使用本文提到的castPtr()castConstPtr(),也不需要花费多大的代价就完成了拷贝。

2.2. 实验2:谨慎使用data()读取数据

实验2与实验1的关键代码差不多,仅仅增加了对varvar1的复制。关键代码如下:

QElapsedTimer profiler;
QVariant var;
QVariant var1;
std::vector<uint8_t> vec(1000000000, 66);
QVector<uint8_t> vec1(1000000000, 66);
profiler.start();
var.setValue(vec);
auto consume1 = profiler.restart();
var1.setValue(vec1);
auto consume2 = profiler.restart();
QVariant varCopy(var);
auto consume3 = profiler.restart();
QVariant var1Copy(var1);
auto consume4 = profiler.restart();
const auto *constPtr = castConstPtr<std::vector<uint8_t>>(var);
auto consume5 = profiler.restart();
const auto *constPtr1 = castConstPtr<QVector<uint8_t>>(var1);
auto consume6 = profiler.restart();
const auto *ptr = castPtr<std::vector<uint8_t>>(var);
auto consume7 = profiler.restart();
const auto *ptr1 = castPtr<QVector<uint8_t>>(var1);
auto consume8 = profiler.restart();
auto value = var.value<std::vector<uint8_t>>();
auto consume9 = profiler.restart();
auto value1 = var1.value<QVector<uint8_t>>();
auto consume10 = profiler.restart();
if (constPtr != nullptr && constPtr1 != nullptr &&
    ptr != nullptr && ptr1 != nullptr) {
    qInfo() << "constPtr[0]" << constPtr->at(0);
    qInfo() << "constPtr1[0]" << constPtr1->at(0);
    qInfo() << "ptr[0]" << ptr->at(0);
    qInfo() << "ptr1[0]" << ptr1->at(0);
    qInfo() << "value[0]" << value.at(0);
    qInfo() << "value1[0]" << value1.at(0);
}
else {
    qCritical() << "cast error!";
}
qInfo() << "var detach" << var.isDetached();
qInfo() << "var1 detach" << var1.isDetached();
qInfo() << "[setValue] for var consume" << consume1 << "ms";
qInfo() << "[setValue] for var1 consume" << consume2 << "ms";
qInfo() << "copy for var consume" << consume3 << "ms";
qInfo() << "copy for var1 consume" << consume4 << "ms";
qInfo() << "[castConstPtr] for var consume" << consume5 << "ms";
qInfo() << "[castConstPtr] for var1 consume" << consume6 << "ms";
qInfo() << "[castPtr] for var consume" << consume7 << "ms";
qInfo() << "[castPtr] for var1 consume" << consume8 << "ms";
qInfo() << "[value] for var consume" << consume9 << "ms";
qInfo() << "[value] for var1 consume" << consume10 << "ms";

程序运行输出的结果如下:

constPtr[0] 66
constPtr1[0] 66
ptr[0] 66
ptr1[0] 66
value[0] 66
value1[0] 66
var detach true
var1 detach true
[setValue] for var consume 205 ms
[setValue] for var1 consume 0 ms
copy for var consume 0 ms
copy for var1 consume 0 ms
[castConstPtr] for var consume 0 ms
[castConstPtr] for var1 consume 0 ms
[castPtr] for var consume 212 ms
[castPtr] for var1 consume 0 ms
[value] for var consume 213 ms
[value] for var1 consume 0 ms

重点解释11-16行:

  • 11-12行:对QVariant整体的拷贝并不耗时,因为Qt使用隐式共享的机制来处理其拷贝。需要说明的是,对于var1(即QVector的holder),对它整体的拷贝并没有使能QVariant的shared状态(即它的is_shared是0,shared->ref.loadRelaxed()返回值是1000000000,跟实验1的结果一样)。怎会如此?我们来看看QVariant的拷贝构造函数实现:

    QVariant::QVariant(const QVariant &p)
        : d(p.d)
    {
        if (d.is_shared) {
            d.data.shared->ref.ref();
        } else if (p.d.type > Char) {
            handlerManager[d.type]->construct(&d, p.constData());
            d.is_null = p.d.is_null;
        }
    }
    

    可以看到,对于原来是is_sharedQVariant,拷贝构造仅仅是对引用计数的加1;而对于原来不是is_sharedQVariant,对它的拷贝并不会使能shared状态,而是直接调用被存储数据的构造函数。因此,例子中的varref变成了2;var1则是执行QVector的拷贝构造函数。因为var1本身的数据就支持隐式共享,所以最终的结果就是varvar1都是以常数时间完成了拷贝构造。

  • 对比13和15行:对于数据的读取而言,constData()相比data()的优势就体现出来了(即这里的castConstPtr()castPtr())。因为对于var,执行data()时真的进行了一次拷贝。所以才有了前面的建议:如果仅仅是为了读取数据,建议选用constData()

  • 对比14和16行:如前所述,因为var1依然不是shared状态,所以执行data()时并不会执行拷贝。所以对它而言,constData()data()没有区别。

3. 修改内部数据

这一节延续上一节的内容。我们将实现第二个需求,即直接对内部数据实现读改写。其实很简单,只需要先使用const_cast强行将constData()转换成void *,然后再用reinterpret_castvoid *转换成目标类型的指针。用函数模板的实现代码如下:

template<typename T>
inline T *castMutablePtr(QVariant & var)
{
    if (var.canConvert<T>()) {
        return reinterpret_cast<T *>(const_cast<void *>(var.constData()));
    }
    return nullptr;
}

需要特别指出,这种方式是一把双刃剑,除非我们真的清楚我们正在做什么,否则尽量避免使用这种方式来直接修改QVariant的内容。在接下来的实验中你明白为什么需要如此谨慎地对待它。另外,Qt的隐式共享机制已经足够强大了,可以应付大部分实际需求。

实践出真知,我们还是做个实验来验证一下。与上一节的设定类似,这里同样分别使用std::vector<uint8_t>QVector<int>创建包含10亿个元素(每个元素的值设置为66)的实例,它们占用内存大小分别约等于953.74MB。我们先分别将数据放入QVariant,并各自拷贝一份,然后使用的castMutablePtr()获取数据指针并分别将容器的第一个元素的值修改为88。实验继续使用QElapsedTimer统计关键操作的耗时。实验的关键代码如下:

QElapsedTimer profiler;
QVariant var;
QVariant var1;
std::vector<uint8_t> vec(1000000000, 66);
QVector<uint8_t> vec1(1000000000, 66);
var.setValue(vec);
var1.setValue(vec1);
QVariant varCopy(var);
QVariant var1Copy(var1);
profiler.start();
auto *ptrMut = castMutablePtr<std::vector<uint8_t>>(var);
auto consume1 = profiler.restart();
auto *ptr1Mut = castMutablePtr<QVector<uint8_t>>(var1);
auto consume2 = profiler.restart();
if (ptrMut != nullptr && ptr1Mut != nullptr) {
    ptrMut->at(0) = 88;
    auto consume3 = profiler.restart();
    ptr1Mut->replace(0, 88);
    auto consume4 = profiler.restart();
    auto value = var.value<std::vector<uint8_t>>();
    auto value1 = var1.value<QVector<uint8_t>>();
    auto valueCopy = varCopy.value<std::vector<uint8_t>>();
    auto value1Copy = var1Copy.value<QVector<uint8_t>>();
    qInfo() << "ptrMut[0]=" << ptrMut->at(0);
    qInfo() << "value[0]=" << value.at(0);
    qInfo() << "valueCopy[0]=" << valueCopy.at(0);
    qInfo() << "ptr1Mut[0]=" << ptr1Mut->at(0);
    qInfo() << "value1[0]=" << value1.at(0);
    qInfo() << "value1Copy[0]=" << value1Copy.at(0);
    qInfo() << "[castMutablePtr] var consume" << consume1;
    qInfo() << "[castMutablePtr] var1 consume" << consume2;
    qInfo() << "[modify] ptrMut consume" << consume3;
    qInfo() << "[modify] ptrMut1 consume" << consume4;
}

程序运行的输出结果如下:

ptrMut[0]= 88
value[0]= 88
valueCopy[0]= 88
ptr1Mut[0]= 88
value1[0]= 88
value1Copy[0]= 66
[castMutablePtr] var consume 0
[castMutablePtr] var1 consume 0
[modify] ptrMut consume 0
[modify] ptrMut1 consume 219

下面则解释一下实验的结果:

  • 1-3行:可以看到,修改通过castMutablePtr()得到的指针指向的内容,导致varvarCopy的内容都受到了影响。所以说,一定要非常谨慎的使用这种方式。
  • 4-6行:var1的情况和var有所不同,修改通过castMutablePtr()得到的指针指向的内容,仅仅影响了var1的内容。这是因为隐式共享机制在这里做了一次copy on write,所以产生了这么大的耗时。
  • 7-8行:上一个小节已经解释清楚了,这里无需赘述。
  • 9-10行:因为对于不支持隐式共享的std::vector不会触发copy on write,节省了一次拷贝,所以第9行展示的耗时才会这么小。但它影响到了varCopy的内容,所以说这是一把双刃剑。而第10行则如前面的解释,发生了copy on write,因此会有这么大的耗时。

4. 总结

本文介绍了使用QVariant::data()QVariant::constData()直接访问QVariant内容的方法,并通过实验证明了其有效性,同时也展现了其局限性。对于数据的读取操作,可以安心地使用QVariant::constData()而无需担心副作用;而对于读改写操作,则需要非常谨慎地使用指针方式。

5. Reference

  1. QVariant Class
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值