使用 Qt Json 库,可以比较方便的实现基于 json 格式的接口通信。然而在通信消息比较频繁的情况下,容易成为效率瓶颈。本文就如何更有效率的使用 Qt Json 库,以及背后的一些相关实现细节做一些讨论。
使用 QtJson 一个例子
QByteArray data;
QJsonDocument doc = QJsonDocument::fromJson(data);
auto json = doc.object();
for (auto elem : json["items"].toArray()) {
auto item = elem.toObject();
auto name = item["name"].toString()
bool enabled = item["status"].toString() == "enabled";
qDebug() << "name" << name << "enabled" << enabled;
}
这段代码解析了下面这样的 json 数据:
{
"items": [
{ "name": "X1", "status": "disabled" },
{ "name": "X2", "status": "enabled" },
{ "name": "X3", "status": "pressed" }
]
}
可是,就这样简简单单的 9 行代码,你知道里面有多少个效率问题吗?
问题1:集合 detach
首先,遍历 Qt 集合,容易引发不必要的集合拷贝。
我们知道 c++ for each 语法,实际上调用了集合的 begin()、end() 方法。如下,QJsonArray 中,每个方法实际上有 非 const 和 const 两个版本:
class Q_CORE_EXPORT QJsonArray
{
....
inline iterator begin() { detach2(); return iterator(this, 0); }
inline const_iterator begin() const { return const_iterator(this, 0); }
...
inline iterator end() { detach2(); return iterator(this, size()); }
inline const_iterator end() const { return const_iterator(this, size()); }
...
}
不注意的话,可能调用了集合的非 const 方法,然而非 const 方法,会调用 detach2(),从而引发集合拷贝。
你可能会奇怪,这个 detach 是什么东东?
其实 Qt 的很多集合都有 detach 机制。从头来说是这样的,Qt 集合使用了 Copy-on-White (COW)机制,也就是多个集合(拷贝的集合)共享内部状态,对于只读操作,共享没有任何问题;一旦某一个拷贝需要改变其状态,就需要拷贝出一份状态,在拷贝的状态上进行修改。
所以可以看到,Qt 集合类的非 const 方法,基本上先要执行 detach。
当然,并不是只要执行 detach 方法,就会拷贝内部状态,只有确实有多个集合共享内部状态时, 才需要拷贝内部状态。不幸的是,使用 QJsonDocument 中的 QJsonArray 肯定是一份拷贝,所以上面的代码必定要拷贝数组的内部状态了。
为解决该问题,可以改成这样:
auto const array = json["items"].toArray();
for (auto const & elem : array)
或者这样:
auto array = json["items"].toArray();
for (auto const & elem : qAsConst(array))
问题2:Object 索引查询效率
对于 QJsonObject 对象,使用字符串 key 去索引是很常见的操作,根据 key 的类型,它有两个版本:
class Q_CORE_EXPORT QJsonObject
{
...
QJsonValue operator[] (QString const & key) const;
QJsonValue operator[] (QLatin1String key) const { return value(key); }
...
}
当使用 QString 作为 Key 时,调用栈是这样的:
然后这个 ucstrncmp 的实现很复杂,用到了一些特殊指令集,有兴趣可以看看这里qstring.cpp source code [qtbase/src/corelib/text/qstring.cpp] - Woboq Code Browser
当使用 QLatin1String 作为 Key 时,调用栈是这样的:
最后的一个函数,是这样实现的,使用了 memcmp 函数。
inline bool operator<(QLatin1String s1, QLatin1String s2) noexcept
{
const int len = qMin(s1.size(), s2.size());
const int r = len ? memcmp(s1.latin1(), s2.latin1(), len) : 0;
return r < 0 || (r == 0 && s1.size() < s2.size());
}
显然,第一种方式,需要比较 Unicode 和 latin 字符集两种字符串,性能劣于简单的 latin 字符串比较。另外,第一种方式,还需要将 key 从 "items" 转换为 QString,QString 需要分配内存来存储 Unicode 字符串,还要进行转换字符集转换,对内存性能、CPU性能都会有影响。
那我们的 json["items"] 到底是调用哪个版本呢?很遗憾,是 QString 作为 key 的版本。为了使用第二种方式的字符串比较,需要将代码改为:
static QLatin1String KEY_ITEMS("items");
auto array = json[KEY_ITEMS].toArray();