本文首发于 BriFuture 的 个人博客

在我的前一篇文章 使用 Qt 获取 UDP 数据并显示成图片 中,我讲了如何用 Python 模拟发送数据,如何在 Qt 中高效的接收 UDP 数据包并将数据解析出来。然而此前的文章在分别显示 RGB 通道、R 通道、G 通道、B 通道这四组通道的图片时仍然会出现处理速度过慢的问题。

前面说过编写的程序至少会用到 3 个线程来分别处理 UI、socket 数据、数据解析,因为不这样做没法在时限内处理完接收到的数据,写第一篇博客的时候,我以为是单纯的使用 new 在堆中分配内存导致程序运行效率低,后来确实通过预分配对象内存解决了部分问题,但是还有一些会影响程序运行速度的问题没有解决,也没有深究,今天重新编写代码的时候,为了分配数据到 4 幅图片上(分别是 RGB 通道、R 通道、G 通道、B 通道),发现运行速度还是不够,影响运行速度的原因有几个:

  1. 运行程序的模式(Debug 和 Release 两种模式)
  2. Qt 的事件循环机制(之前反复怀疑过,不过最后还是发现短时间内大量调用信号很容易导致处理速度过慢)
  3. 低效的内存复制操作(如 QByteArray 的 assign 赋值操作和过多、过于复杂的程序流程)

接下来看看这几个导致程序运行速度不够的原因:

1. Qt Debug 模式和 Release 模式的差异

在 QtCreator 中运行程序,如果是以 Debug 模式运行的话,速度是要比 Release 模式低一些的。以前编写 Qt 程序,数据量一般不大,对于性能都没有要求,即使程序代码不够优化,但在用户使用过程中一般不会感受到运行卡顿,所以一直都没发现 Debug 模式和 Release 模式的性能有差异。

不过其实也能猜到性能有差异的大概原因:Debug 模式下会在最终生成的代码里面插入很多额外的代码用于调试,但是 Release 生成的代码是不会插入这些调试用的代码的,最明显的差异就是 Debug 模式生成的可执行文件比 Release 模式生成的可执行文件要大得多。

Debug 模式下运行程序,实际 FPS 和期望的 FPS 有 6 帧的差距,差距产生的原因是处理速度不够,导致最终生成图片的速度慢了。

Release 模式下即使是原始数据包的期望 FPS 到了 77 帧,实际的 FPS 也可以达到 77 帧,也就是说在处理过程中没有出现处理速度跟不上接受数据的速度。

2. Qt 的事件循环机制

当我们使用 Qt 程序的时候,经常会在主函数 main 里写出类似下面这样的代码:

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow mw;
mw.show();
return a.exec();
}

这样我们的程序应用的生命周期就是 QApplication 所定义的,当我们使用 QObject::moveToThread 方法将某个 QObject (子类)对象移到其他的子线程中的时候,子线程也有独立于主线程的相应的事件派发机制。

QObject 的多线程使用方法很巧妙,利用信号&槽机制或者是 QMetaObject::invokeMethod 方法就可以让要执行的耗时函数在子线程中执行,但是如果是直接调用耗时函数,那么就会在当前的线程中执行耗时操作,导致线程阻塞。

在线程之间传递数据,如果是用信号&槽的机制,那么可能你都不需要考虑线程间的数据同步问题,但信号&槽机制是要依靠 Qt 的事件循环机制的,如果事件不能正常分发(dispatch),那么子线程中的槽函数就不会被调用。

关于 Qt 的事件循环机制和线程机制,推荐看一看官方 wiki,《线程、事件与QObject》 或者也有对应的英文原文 Threads Events QObjects

如果频繁的调用信号,在 Qt 的事件循环中,因为前一次耗时的任务没有完成,导致对应的槽函数无法执行,最终导致处理速度跟不上。

因此对于实时性要求高的程序,Qt 的事件循环机制可能不会是你的首选,你更有可能去做的是在 Qt 的一个子线程中运行循环代码,忽略掉该子线程中的事件循环以提高程序的性能。

3. 低效的内存复制操作

在接收到原始字节数据之后,最重要也是最麻烦的就是解析数据。包括识别自定义协议数据的头部信息,将数据包中的图像数据复制到缓冲区,并将缓冲区中的数据以图片的形式显示出来。接下来分享几个高效处理数据的几个小技巧:

  1. 使用 QByteArray 存储原始数据包时,先调用 resize 预分配内存,然后使用 memcpy 直接对内存数据进行操作,这样做效率是最高的,但它也是比较繁琐的。
QByteArray data;
data.resize(PacketSize); // PacketSize 是预先定义好的数据字节数
// 可以简单的认为 rawData 就是从 UDP 端口中接收到的数据,
// ValidDataSize 也是预先定义好的数据字段的长度
memcpy(rawData.data(), data.data(), ValidDataSize); // 上面的代码要比直接使用赋值操作 = 高效
data = rawData;
  1. 如果接收到的数据可以明确是有序的,可以用数据分别表示相应的序号,再从数组中取数据,我最开始存储 LineDataObj (用于表示图片的一行数据)的时候,用 QMap 存储行号和指针,利用 QMap 的查找功能减少了查找或排序的时间,但是缺点是 QMap 会随着其内部的数据量增大变得缓慢,如果只需要缓存数据,建议直接使用数组存取,这样的运行效率最高。
// 在类中声明一个 map
QMap<int line, LineDataObj *> map; // 在方法体中使用 map 查找是否有对应的行数据
if(map.contains(line)) {
// 如果有对应的行数据对象,直接将数据写入到行对象数据上
...
} else {
// 如果没有,则插入一条记录
map.insert(line, lineDataObj);
} // 处理完一行数据后,可以将该行数据从 map 中移除掉
map.remove(line);

可以发现,map 就是用来判断是否有对应行数据对象,然后处理结束后移除保存的行号,这并没有达到缓存数据的目的,反而再插入和移除的过程中浪费了过多时间。但如果用一个数组当做缓存区就会快很多,因为我们减少了查找和移除记录的时间:

QVector<LineDataObj *> linePool;
// LinePoolSize 是预定义的池大小
linePool.reserve(LinePoolSize);
for(int i = 0; i < LinePoolSize; i++) {
linePool.append(new LineDataObj());
} // 数组大小是有限的,行号却是不断增加的,因此要设置一个起始行,保证在长时间执行程序后不会出现数组越界的问题
int diffLine = line - startLine;
// 进行处理
linePool[line].setData(...);
  1. 尽量保持清晰而且简单的结构。我之前写代码总想着考虑到所有情况,最终却总是没法尽善尽美只有根据情况放低预期,我觉得不必一开始就非要把代码的层次结构划分的特别详细,根据实际情况使用合理的程序结构(当然每个人可能有不同的看法,但少即是多的原则确实给了我很大的启发)。

我之前编写程序时,除了有一个 LineDataObj 用来表示行对象,还有一个 RawDataObj 表示原始的数据包对象。处理的流程多:1. 接受原始数据包 => 2. 将数据包填充到 RawDataObj 中并解析数据包的行号,RGB 类型 => 3. 根据 RawDataObj 的属性确定对应的 LineDataObj => 4. 当 LineDataObj 存储到一定数目时生成图像。

这个流程很直观也很容易想到,但是 RawDataObj 这个数据对象其实没必要使用,因为它增加了一次不必要的内存数据复制。这完全可以给 LineDataObj 类增加几个静态方法,判断出数据包的行号和 RGB 类型,然后将数据部分写入到 LineDataObj 的数据字段中。这样做不仅可以减少内存读写的次数,而且可以在一个对象中申请大段内存,保存整行的数据,最后写入到图片时,只用将这个区域赋值到图片中即可。

4. 高效地显示图片

最后分享一下如何在 Qt 中高效的显示图片。一般用 Qt 显示图片可以用 QLabel:

QLabel label;
QImage image;
// 执行一些读取图片的操作,再显示在 QLabel 上
label.setPixmap(QPixmap::fromImage(image));

但是用 QPixmap::fromImage 会从 image 的内存区域中复制一份数据到 Pixmap 中,这样的操作并不高效。我们可以使用 QImage::scanLine 方法获取它对应的内存区域,直接对内存进行操作,显示的时候不用 QPixmap::fromImage,我们要直接将内存中的修改显示到界面中,这样我们要定义一个类(不妨让它继承 QLabel),重写 paintEvent 方法:

void PictureImage::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
if(m_index == uchar(-1)) {
return;
}
// this->painter.drawImage(target, *m_image);
QPainter p(this);
// target 在构造函数中定义:
// target = QRectF(0.0, 0.0, PictureImage::ImageWidth, PictureImage::ImageHeight);
p.drawImage(target, *m_images[m_index]);
}

p.drawImage(target, image) 这样就可以将图片更新到界面中,并且它会被 QPixmap 的 fromImage 方法要高效。

用 Python 发送模拟数据遇到的问题

之前说过,模拟数据是用 Python 代码编写的,这个代码发送模拟数据的效率可以高达 100M/s,下面的截图是我在自己的笔记本(i5 8200U@1.8G)上运行的结果:

但是令我感到特别奇怪的是,有一段时间同样的代码在我的 amd ryzen 1500x@3.5G 台式机上只能达到 50M/s 的速度。我一度怀疑是英特尔和 AMD 的处理器单核性能有差异,但按道理不应该有这么大的速度差异。而且最近几天它又在我的台式机上能够跑到 100M/s 的速度。

参考

线程、事件与QObject

使用 Qt 获取 UDP 数据并显示成图片(2)的更多相关文章

  1. 使用 Qt 获取 UDP 数据并显示成图片

    一个项目,要接收 UDP 数据包,解析并获取其中的数据,主要根据解析出来的行号和序号将数据拼接起来,然后将拼接起来的数据(最重要的数据是 R.G.B 三个通道的像素值)显示在窗口中.考虑到每秒钟要接收 ...

  2. base64格式的图片数据如何转成图片

    base64格式的图片数据如何转成图片 一.总结 一句话总结:不仅要去掉前面的格式串,还需要base64_decode()解码才行. // $base_img是获取到前端传递的值 $base_img ...

  3. C# 在网页中将Base64编码的字符串显示成图片

    在写一个接口,返回的json里面有图片,是Base64编码的字符串. 测试接口的时候,发现原来在html显示,是直接可以将Base64编码的字符串显示成图片的. 格式如下: <img src=d ...

  4. android 从服务器获取新闻数据并显示在客户端

    新闻客户端案例 第一次进入新闻客户端需要请求服务器获取新闻数据,做listview的展示, 为了第二次再次打开新闻客户端时能快速显示新闻,需要将数据缓存到数据库中,下次打开可以直接去数据库中获取新闻直 ...

  5. Opencv+MFC获取摄像头数据,显示在Picture控件

    分为两步:OpenCV获取摄像头数据+图像在Picture上显示 第一步:OpenCV获取摄像头数据 参考:http://www.cnblogs.com/epirus/archive/2012/06/ ...

  6. 获取minist数据并转换成lmdb

    caffe本身是没有数据集的,但在data目录下有获取数据的一些脚本.MNIST,一个经典的手写数字库,包含60000个训练样本和10000个测试样本,每个样本为28*28大小的黑白图片,手写数字为0 ...

  7. .net后台获取DataTable数据,转换成json数组后传递到前台,通过jquery去操作json数据

    一,后台获取json数据 protected void Page_Load(object sender, EventArgs e){  DataTable dt = DBhepler.GetDataT ...

  8. 创建一个Java Web项目,获取POST数据并显示

    新建一个新的Java Web工程项目 打开IntelliJ IDEA 新建一个工程,选择选择Java Enterprise,设置Tomcat的安装目录,点击下一步. 选中Create project ...

  9. TensorFlow笔记五:将cifar10数据文件复原成图片格式

    cifar10数据集(http://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz)源格式是数据文件,因为训练需要转换成图片格式 转换代码: 注意文件路 ...

随机推荐

  1. robot中使用evaluate转化数据格式

    如果你使用robot却没有用过evaluate,那你将永远禁锢在框架中. json对象格式入参可以使用字典格式直接传入,但最近有一个接口测试的入参是一个json数组,在传参时总是提示请求参数不合法, ...

  2. RobotFramework与Jenkins集成后失败用例重跑

    Jenkins的执行Windows批处理命令填写如下: call pybot.bat -i 1adsInterface 01_测试用例\接口测试用例\adsInterface.txt call pyb ...

  3. C# 设置textedit只能输入英文数字下划线,并且只能以英文开头(正则表达式)

    this.textEdit1.Properties.Mask.EditMask = @"[a-zA-z][a-zA-Z0-9_]*";

  4. Asp.Net Core下的两种路由配置方式

    与Asp.Net Mvc创建区域的时候会自动为你创建区域路由方式不同的是,Asp.Net Core下需要自己手动做一些配置,但更灵活了. 我们先创建一个区域,如下图 然后我们启动访问/Manage/H ...

  5. CentOS6.9 minimal版本安装图形化界面

    CentOS6.9 minimal版本安装图形化界面 安装步骤如下: 1.安装Desktop组 # yum groupinstall "Desktop" -y 2.安装X Wind ...

  6. UIResponder笔记

    UIResponder是什么 可以响应UIEvent的类,是UIApplication, UIView及UIViewController的父类.它的父类是NSObject 管理第一响应者. 是否是第一 ...

  7. dotnet --info

    [root@bogon ~]# dotnet --info.NET Command Line Tools (2.1.4) Product Information: Version: 2.1.4 Com ...

  8. redis中存储小数

    在做一个活动的需求时,需要往redis中有序的集合中存储一个小数,结果发现取出数据和存储时的数据不一致 zadd test_2017 1.1 tom (integer) zrevrange test_ ...

  9. leetcode-665-Non-decreasing Array

    题目描述: Given an array with n integers, your task is to check if it could become non-decreasing by mod ...

  10. (USB HID) Report Descriptor 理解

    在這理整理一下基本 Report Descriptor 對於入門基礎的了解. 在很多文件.Blog都有提到HID report 總共分為3種 : Input.Output.Feature report ...