Qt Signals Slots VS QEvents - Qt跨线程异步操作性能测试与选取建议

相关代码参考:https://gitcode.net/coloreaglestdio/qtcpp_demo/-/tree/master/qt_event_signal

1.问题的由来

在对 taskBus 进行低延迟改造时,避免滥用信号与槽起到了较好的作用。笔者在前一篇文章中,叙述了通过避免广播式地播发信号,以及频繁的 new 与 delete 来提高软件无线电(SDR)平台的吞吐。近期,考虑到跨线程异步操作其实事件(QEvent)可能更加适合点对点的调用,遂把taskBus的主数据流转使用 Events 进行了改造,收到了大概5-10ms的提升。

这个提升还是没有达到我的期望,因为印象里,信号与槽是非常慢的。那么,问题来了,跨线程 Signal&Slots 与 Events 到底谁要快,为什么快,以及快多少呢?

2. 回顾“信号槽很慢”的印象的源头

在经典的Qt文档里,有一段对信号与槽性能选择的性能描述:

Compared to callbacks, signals and slots are slightly slower because of the 
increased flexibility they provide, although the difference for real applications 
is insignificant. In general, emitting a signal that is connected to some slots,is approximately ten times slower than calling the receivers directly, with non-virtual function calls.

“与回调相比,信号和插槽的速度稍慢,因为它们提供了更大的灵活性,尽管实际应用程序的差异并不显著。通常,发射连接到某些插槽的信号比直接调用接收器(使用非虚拟函数调用)慢大约十倍。”

这就是印象的源头了。不过,带着上述问题,仔细阅读,发现还有更多的解释:

 This is the overhead required to locate the connection object, to safely iterate over all connections (i.e. checking that subsequent receivers have not been destroyed during the emission), and to marshall any parameters in a generic fashion. While ten non-virtual function calls may sound like a lot, it's much less overhead than any new or delete operation, for example.As soon as you perform a string, vector or list operation that behind the scene requires new or delete, the signals and slots overhead is only responsible for a very small proportion of the complete function call costs. The same is true whenever you do a system call in a slot; or indirectly call more than ten functions. The simplicity and flexibility of the signals and slots mechanism is well worth the overhead, which your users won't even notice.

“这是定位连接对象、安全地迭代所有连接(即检查后续接收器在发射过程中是否未被破坏)以及以通用方式整理任何参数所需的开销。虽然十个非虚拟函数调用听起来可能很多,但它的开销比任何new操作或delete操作都要小得多。一旦执行了一个字符串、向量或列表操作,而该操作在后台需要新建或删除,则信号和插槽开销只占整个函数调用成本的一小部分。每当您在插槽中进行系统调用时,情况也是如此;或者间接调用十多个函数。信号和插槽机制的简单性和灵活性非常值得开销,而您的用户甚至不会注意到这一点。”

会不会,上次的优化起到关键作用的不是信号与槽改成了直接函数调用,而是把频繁new delete换成静态内存导致的提升?我立刻进行了测试,发现的确如此。

  • new 和 delete的开销远远大于 signal&slots的开销

3. 编程进行专门测试

我们使用Qt专门对信号与槽、事件这两种跨线程传递消息的方法来进行测试。设计测试由两个对象之间互相以最快速度乒乓消息为场景,如下图所示:

时戳消息
时戳消息
Object1
Object2

含有时戳的消息,可以用来计算和统计平均延迟(消息生成和被处理的时差),以及最终的处理能力。

测试分为信号-槽测试,事件测试,以及单线程对比调用测试。测试进行10000次调用,并模拟实际程序对传递的消息进行一些处理,如产生一定长度的字符串。

通过观察不同长度下的开销,即可直挂感受消息传递开销与处理开销的占比。

3.1 用于测试的消息体

消息体既用于信号与槽测试,也用于Event测试以及直接调用。这样参数中都直接new消息,对大家是公平的。

testevent.h---------------#include <QEvent>
#include <time.h>
#include <QString>
class TestMsg : public QEvent
{
private:static QEvent::Type m_testEvt;static QEvent::Type m_startEvt;static QEvent::Type m_startSig;static QEvent::Type m_quit;clock_t m_clk = 0;QString m_dummyLongStr;
public:TestMsg(clock_t clk);~TestMsg();inline clock_t clock() {return m_clk;}inline void fillStr(int len){for (int i=0;i<len;++i){m_dummyLongStr.push_back((char)(i*37%64+32));}}
public:static inline QEvent::Type type() {return m_testEvt;}static inline QEvent::Type startEvt() {return m_startEvt;}static inline QEvent::Type startSig() {return m_startSig;}static inline QEvent::Type quitEvt() {return m_quit;}
};//testevent.cpp---------------
#include "testevent.h"
#include <QDebug>
//Regisit a event
QEvent::Type TestMsg::m_testEvt = (QEvent::Type)QEvent::registerEventType();
QEvent::Type TestMsg::m_startEvt = (QEvent::Type)QEvent::registerEventType();
QEvent::Type TestMsg::m_startSig = (QEvent::Type)QEvent::registerEventType();
QEvent::Type TestMsg::m_quit = (QEvent::Type)QEvent::registerEventType();TestMsg::TestMsg(clock_t clk):QEvent(m_testEvt),m_clk(clk)
{}
TestMsg::~TestMsg()
{}

消息体含有一个clock时戳,用于计算传递耗时。同时,有一个fillStr的耗时操作,用于模拟真实的有效数据处理。

3.2 测试对象

测试对象直接派生自 QObject,将在线程中执行测试。为了模拟信号与槽的 meta 开销,设置了26组信号与槽,进行交叉连接,并用第16组进行测试。

#ifndef TESTOBJ_H
#define TESTOBJ_H#include <QObject>
#include <QEvent>
#include "testevent.h"
class TestObj : public QObject
{Q_OBJECT
public:explicit TestObj(QObject *parent = nullptr);void setBuddy(TestObj * buddy) {m_buddy = buddy;buddy->m_buddy = this;}void runDirectCall();
public slots:void test_slot1(QEvent * evt){}//...void test_slot16(QEvent * evt);//...void test_slot26(QEvent * evt){}
signals:void test_sig1(QEvent * evt);//...void test_sig26(QEvent * evt);void evt_finished();void sig_finished();
protected:void customEvent(QEvent *) override;
private:clock_t m_nFirstClkSig = -1;quint32 m_nCount_Sigs = 0;clock_t m_nFirstClkEvt = -1;quint32 m_nCount_Evts = 0;clock_t m_nFirstClkDir = -1;quint32 m_nCount_Dir = 0;
private:TestObj * m_buddy = nullptr;
private:void run_signal();void run_event();
private:void direct_call(QEvent * evt);
};#endif // TESTOBJ_H

无论以何种接口获得 TestMsg,都执行相同的操作:

//以信号槽为例
void TestObj::test_slot16(QEvent * evt)
{clock_t curr_clk = clock();if (m_nFirstClkSig==-1)m_nFirstClkSig = curr_clk;TestMsg * e = dynamic_cast<TestMsg *>(evt);if (e){++m_nCount_Sigs;e->fillStr(fillStrLen);if (m_nCount_Sigs==testCounts){QTextStream strm(stdout);strm << objectName()<< QString().asprintf(" (%llX) run %d Signals, total costs %.2lf ms, AVG cost %.2lf us / test.\n",(unsigned long long)this,(int)(m_nCount_Sigs),1e3 * (curr_clk - m_nFirstClkSig) / CLOCKS_PER_SEC,1e6 * (curr_clk - m_nFirstClkSig) / CLOCKS_PER_SEC / testCounts);strm.flush();emit sig_finished();}delete e;}
}void TestObj::run_signal()
{for (int i=0;i<testCounts;++i)emit test_sig16(new TestMsg(clock()));
}

3.3 测试方法

在 main函数中进行测试:

#include <QCoreApplication>
#include <QThread>
#include "testevent.h"
#include "testobj.h"
int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);clock_t clk_start = clock();printf("StartClk = %d\n",clk_start);QThread::msleep(100);QThread * thread1 = new QThread;QThread * thread2 = new QThread;TestObj * obj1 = new TestObj;TestObj * obj2 = new TestObj;obj1->setObjectName("OBJ1");obj2->setObjectName("OBJ2");obj1->setBuddy(obj2);obj1->connect(obj1,&TestObj::test_sig1,obj2,&TestObj::test_slot16,Qt::QueuedConnection);//...obj1->connect(obj1,&TestObj::test_sig26,obj2,&TestObj::test_slot16,Qt::QueuedConnection);obj1->connect(obj2,&TestObj::test_sig16,obj1,&TestObj::test_slot1,Qt::QueuedConnection);//...obj1->connect(obj2,&TestObj::test_sig16,obj1,&TestObj::test_slot26,Qt::QueuedConnection);obj1->moveToThread(thread1);obj2->moveToThread(thread2);thread1->start();thread2->start();printf("Test signals & slots, Events...\n");QCoreApplication::processEvents();a.connect (obj1,&TestObj::sig_finished,[=]()->void{QThread::msleep(1000);QCoreApplication::postEvent(obj1,new QEvent(TestMsg::startEvt()));});a.connect (obj2,&TestObj::sig_finished,[=]()->void{QThread::msleep(1000);QCoreApplication::postEvent(obj2,new QEvent(TestMsg::startEvt()));});a.connect (obj1,&TestObj::evt_finished,[=]()->void{QThread::msleep(1000);QCoreApplication::postEvent(obj1,new QEvent(TestMsg::quitEvt()));});a.connect (obj2,&TestObj::evt_finished,[=]()->void{QThread::msleep(1000);QCoreApplication::postEvent(obj2,new QEvent(TestMsg::quitEvt()));});QThread::msleep(1000);QCoreApplication::processEvents();QCoreApplication::postEvent(obj1,new QEvent(TestMsg::startSig()));QCoreApplication::postEvent(obj2,new QEvent(TestMsg::startSig()));a.processEvents();thread1->wait();thread2->wait();QThread::msleep(2000);obj1->runDirectCall();printf("Finished.\n");thread1->deleteLater();thread2->deleteLater();obj1->deleteLater();obj2->deleteLater();QThread::msleep(1000);QCoreApplication::processEvents();return 0;
}

4. 测试结果

测试环境:i7 10代 win11 Mingw64 Qt6.6

4.1 Debug, Strlen=0,20000次

StartClk = 2
Test signals & slots, Events...
OBJ1 (1AD52AD73E0) run 20000 Signals, total costs 2089.00 ms, AVG cost 104.45 us / test.
OBJ2 (1AD52AD74C0) run 20000 Signals, total costs 32.00 ms, AVG cost 1.60 us / test.
OBJ2 (1AD52AD74C0) run 20000 Events, total costs 14.00 ms, AVG cost 0.70 us /test.
OBJ1 (1AD52AD73E0) run 20000 Events, total costs 27.00 ms, AVG cost 1.35 us /test.
Test Direct Call...
OBJ1 (1AD52AD73E0) run 20000 Direct Calls, total costs 6.00 ms, AVG cost 0.30 us / test.
Finished.

4.2 Debug, Strlen=1000,20000次

StartClk = 2
Test signals & slots, Events...
OBJ1 (21AFB83BBA0) run 20000 Signals, total costs 2541.00 ms, AVG cost 127.05 us / test.
OBJ2 (21AFB83B820) run 20000 Signals, total costs 1630.00 ms, AVG cost 81.50 us / test.
OBJ2 (21AFB83B820) run 20000 Events, total costs 1373.00 ms, AVG cost 68.65 us /test.
OBJ1 (21AFB83BBA0) run 20000 Events, total costs 1403.00 ms, AVG cost 70.15 us /test.
Test Direct Call...
OBJ1 (21AFB83BBA0) run 20000 Direct Calls, total costs 1377.00 ms, AVG cost 68.85 us / test.
Finished.

4.3 Release, Strlen=0,100000次

StartClk = 5
Test signals & slots, Events...
OBJ1 (294D24BB5F0) run 100000 Signals, total costs 2236.00 ms, AVG cost 22.36 us / test.
OBJ2 (294D24BB930) run 100000 Signals, total costs 25.00 ms, AVG cost 0.25 us / test.
OBJ2 (294D24BB930) run 100000 Events, total costs 12.00 ms, AVG cost 0.12 us /test.
OBJ1 (294D24BB5F0) run 100000 Events, total costs 38.00 ms, AVG cost 0.38 us /test.
Test Direct Call...
OBJ1 (294D24BB5F0) run 100000 Direct Calls, total costs 14.00 ms, AVG cost 0.14 us / test.
Finished.

4.4 Release, Strlen=1000,100000次

StartClk = 4
Test signals & slots, Events...
OBJ1 (154799851A0) run 100000 Signals, total costs 1358.00 ms, AVG cost 13.58 us / test.
OBJ2 (154799854E0) run 100000 Signals, total costs 620.00 ms, AVG cost 6.20 us / test.
OBJ2 (154799854E0) run 100000 Events, total costs 543.00 ms, AVG cost 5.43 us /test.
OBJ1 (154799851A0) run 100000 Events, total costs 547.00 ms, AVG cost 5.47 us /test.
Test Direct Call...
OBJ1 (154799851A0) run 100000 Direct Calls, total costs 527.00 ms, AVG cost 5.27 us / test.
Finished.

5. 结果分析

可以看见,

  • Release下,Event 和 直接调用的性能几乎完全一样。Debug下略慢。
  • Release下,如果发射的信号只对应一个槽,则比直接调用的性能稍微差一些,但远没有10倍的差距,比如Obj2发射的某个信号,只连接到Obj1 的1个槽,其实区别不大。
  • Release下,如果发射的信号对应多个槽,则性能显著下降,正如obj1发射的一个信号,连接到多个槽,结果就很耗时。发射的信号会给每个槽都走一遭。
  • 如果存在大量的new\delete,则无论何方式,区别都不大。new/delete非常耗时。

6. 开发建议

对于密集的点对点异步调用,显然是Event比较好。如果是广播性质的多对多,Event需要循环,则使用信号与槽会利于开发。此外信号与槽会自动维护双方的可用性,在目的析构后不再调用槽。Events因为要给入指针,则必须自己确保指针的有效性。

提高速度的关键还是使用静态内存,避免频繁new、delete。

7. 改进效果

taskBus的异步调用最后一步队列操作,使用Event而不是信号,来通知线程干活,只获得了大概10ms的延迟优化,是因为大部分延迟实际是在缓存、滤波器上,并不在信号-槽中。使用了QEvent后,构造的全通无线网络,在 128kbps的带宽下,还是要稍微顺畅一些。

Gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/168355.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

行业模型应该如何去拆解?

行业模型应该如何去拆解&#xff1f; 拆解行业模型是一个复杂的过程&#xff0c;涉及对整个行业的深入分析和理解。下面是一些步骤和方法&#xff0c;可以帮助你系统地拆解行业模型&#xff1a; 1. 确定行业范围 定义行业&#xff1a;明确你要分析的行业是什么&#xff0c;包括…

QT计时器

widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTimerEvent> //计时器类 #include <QTime> //时间类 QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic:Widg…

【Proteus仿真】【Arduino单片机】蜂鸣器

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真Arduino单片机控制器&#xff0c;使用蜂鸣器等。 主要功能&#xff1a; 系统运行后&#xff0c;蜂鸣器播放音乐。 二、软件设计 /* 作者&#xff1a;嗨小易&#xff08;QQ&#x…

H3C SecParh堡垒机 data_provider.php 远程命令执行漏洞

构造poc执行远程命令&#xff1a; /audit/data_provider.php?ds_y2019&ds_m04&ds_d02&ds_hour09&ds_min40&server_cond&service$(id)&identity_cond&query_typeall&formatjson&browsetrue漏洞证明&#xff1a; 文笔生疏&#xff0c…

canvas绘制动态视频并且在视频上加上自定义logo

实现的效果&#xff1a;可以在画布上播放动态视频&#xff0c;并且加上自定义的图片logo放在视频的右下角 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthd…

初识Java

一、Java语言概述 1.1 Java是什么 Java是一种优秀的程序设计语言&#xff0c;它具有令人赏心悦目的语法和易于理解的语义 不仅如此&#xff0c;Java还是一个有一系列计算机软件和规范形成的技术体系&#xff0c;这个技术体系提供了完整的用于软件开发和跨平台部署的支持环境&a…

yolov8x-p2 实现 tensorrt 推理

简述 在最开始的yolov8提供的不同size的版本&#xff0c;包括n、s、m、l、x&#xff08;模型规模依次增大&#xff0c;通过depth, width, max_channels控制大小&#xff09;&#xff0c;这些都是通过P3、P4和P5提取图片特征&#xff1b; 正常的yolov8对象检测模型输出层是P3、…

【微服务】Feign 整合 Sentinel,深入探索 Sentinel 的隔离和熔断降级规则,以及授权规则和自定义异常返回结果

文章目录 前言一、Feign 整合 Sentinel1.1 实现步骤1.2 FallbackFactory 示例 二、Sentinel 实现隔离2.1 隔离的实现方法2.2 Sentinel 实现线程隔离示例 三、熔断降级规则3.1 熔断降级原理及其流程3.2 熔断策略 —— 慢调用3.3 熔断策略 —— 异常比例和异常数 四、授权规则4.1…

文件的基本操作(创建文件,删除文件,读写文件,打开文件,关闭文件)

1.创建文件(create系统调用) 1.进行Create系统调用时&#xff0c; 需要提供的几个主要参数: 1.所需的外存空间大小&#xff08;如:一个盘块&#xff0c;即1KB) 2&#xff0e;文件存放路径&#xff08;“D:/Demo”) 3.文件名&#xff08;这个地方默认为“新建文本文档.txt”) …

计算机网路第3章-运输层

概述和运输层服务 运输层协议为运行在不同主机上的应用进程提供了逻辑通信&#xff0c;从应用程序角度看&#xff0c;通过使用逻辑通信&#xff0c;就好像运行在不同主机上的进程直接相连在一起一样。 运输层和网络层的关系 网络层提供主机之间的通信&#xff0c;而运输层提…

期中考misc复现

第一题 flow analysis 1 服务器附带的后门文件名是什么&#xff1f;&#xff08;包括文件后缀&#xff09; Windows后门是指当攻击者通过某种手段已经拿到服务器的控制权之后&#xff0c;然后通过在服务器上放置一些后门&#xff08;脚本、进程、连接之类&#xff09;&#xf…

SpringBoot Lombok的使用

目录 下载Lombok插件 Lombok的用法 获取日志对象 生成get,set方法 Lombok框架的实现原理 Lombok的常用注解 下载Lombok插件 要使用Lombok首先要确保idea安装了lombok插件 在项目中添加 lombok依赖 在<dependency>里右键生成点击edit starters 插件(没有就下载,可…

分治法,动态规划法,贪心法,回溯法主要概括

目录 分治法&#xff0c;动态规划法&#xff0c;贪心法&#xff0c;回溯法主要概括 1.前言2.分治法2.1基本思想&#xff1a;2.2适用条件&#xff1a;2.3时间复杂度&#xff1a;2.4主要解决&#xff1a;2.5关键字&#xff1a;2.6其他&#xff1a; 3.动态规划法3.1基本思想&…

使用序列化技术保存数据 改进 IO流完成项目实战水果库存系统

上一节内容是 使用IO流完成项目实战水果库存系统https://blog.csdn.net/m0_65152767/article/details/133999972?spm1001.2014.3001.5501 package com.csdn.fruit.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java…

基于stm32控制的4G模块在设备模式下通讯

这里的32控制其实和51的控制思路都是一样的&#xff0c;都是先利用一个网络助手将家里的无线网生成局域网&#xff0c;接着通过花生壳软件将局域网变成公共网&#xff0c;最后是利用串口助手&#xff0c;在4G模块的AT指令模式写入命令ATSOCKTCPC,公共网IP地址,公共网端口号&…

Hadoop3教程(二十七):(生产调优篇)HDFS读写压测

文章目录 &#xff08;146&#xff09;HDFS压测环境准备&#xff08;147&#xff09;HDFS读写压测写压测读压测 参考文献 &#xff08;146&#xff09;HDFS压测环境准备 对开发人员来讲&#xff0c;压测这个技能很重要。 假设你刚搭建好一个集群&#xff0c;就可以直接投入生…

跨境商城源码可以支持多种用户权限管理方式吗?

在跨境电商领域&#xff0c;为了满足消费者的需求和提供更好的购物体验&#xff0c;开发人员需要使用一种可靠的跨境商城源码来构建跨境电商平台。然而&#xff0c;这个商城源码是否可以支持多种用户权限管理方式呢?本文将探讨这个问题。 1.什么是跨境商城源码? 跨境商城源码…

linux进程管理,一个进程的一生(喂饭级教学)

这篇文章谈谈linux中的进程管理。 一周爆肝&#xff0c;创作不易&#xff0c;望支持&#xff01; 希望对大家有所帮助&#xff01;记得收藏&#xff01; 要理解进程管理&#xff0c;重要的是周边问题&#xff0c;一定要知其然&#xff0c;知其所以然。看下方目录就知道都是干货…

【单片机学习笔记】Windows+Vscode+STM32F4+freeRTOS+FatFs gcc环境搭建

为摒弃在接受keil邮件&#xff0c;研究了下gun编译&#xff0c;以STM32F407为例&#xff0c;简单记录 1. 软件包准备 Git 选择对应版本直接安装即可https://git-scm.com/download/winmakegcc ​ 1&#xff09;将上述软件包放置于C盘根目录 2&#xff09;添加环境变量 3&am…

【Git】idea提交项目到Gitee

文章目录 1. 创建Gitee仓库1. 新建仓库2. 添加描述3. 复制仓库地址 2. idea建立连接提交2.1 Create Git Repository2.2 选择要提交的根目录2.3 Commit2.4 Push2.5 提交成功 1. 创建Gitee仓库 1. 新建仓库 2. 添加描述 3. 复制仓库地址 点击右上角克隆/下载&#xff0c;复制HT…