Qt 性能优化 现象和解决方案

一、前言

在最近的项目中,发现执行 Qt 程序时,有些情况下的 CPU 占用率奇高,最高高达 100%。项目跑在嵌入式板子上,最开始使用 EGLFS 插件,但是由于板子没有单独的鼠标层,导致鼠标移动起来卡顿,很不流畅,所以换成了 LinuxFB 插件。但是如果 CPU 占有率高了的话,也会导致鼠标卡顿,因为鼠标是由 Qt 应用程序层负责绘制的,如果应用程序 CPU 占有率高了的话,也会导致鼠标绘制缓慢,导致鼠标卡顿。

所以要解决 CPU 占有率高的问题,进行性能优化,本文对项目中容易出现的问题现象与解决办法进行了记录。

二、UI 控件频繁刷新

现象

代码逻辑控制的 UI 控件(包括 QWidget)频繁刷新,或者某个动作频繁执行。UI 刷新在一定程度上会对CPU占用率有影响,若频繁刷新控件(不论是重绘还是更新数据),都会十分消耗 CPU 占用率。例如一个时间 Label,采用定时器设置时间,如果定时器的定时间隔过短,比如 10ms 甚至更短,就会比较消耗 CPU 占用率。

解决方法

所以自定义 UI 控件时就需要注意避免频繁刷新;定时器的间隔时间也要合理设置,避免过短或过长。

三、paintEvent 高频绘图

一、不太推荐使用 QPainter 绘制视频

在 Qidget 上显示视频,一般是通过 paintEvent,由于 QPainter 不是用来渲染视频的,是用来自绘 GUI 的,用的是 CPU 渲染,而不是 GPU,缺乏显卡加速,性能达不到绘制视频的要求。另外,视频绘制也不应该使用事件驱动,使用事件驱动会让绘制操作运行在主线程里,这有可能阻塞 UI 事件响应,显然是不合理的。另外还有以下缺陷:

  • YUV 转 RGB 转换需要消耗大量 CPU;
  • QPainter 大面积绘制效率不高,测试过,如果只是些小预览窗口绘图还行,如果是大的窗口,越大越消耗 CPU,至于全屏则是会卡到爆。

二、推荐使用 QOpenGLWidget 绘制视频

现在一般性能好点的嵌入式板子都有 GPU,支持 OpenGL的话,最推荐使用 Qt 官方自带的 QOpenGLWidget 来绘图。

具体就是显示视频的那个类, 让它继承 QOpenGLWidget,这样使用的是 GPU 渲染,然后重新使用 paintEvent 函数,就不怎么消耗 CPU 了。

三、CPU 绘图推荐使用 SDL、OpenCV 等第三方库

拿到 widget 的窗口句柄(好像是 winid() 这个函数的返回值)或者窗口位置,配合其他工具(opengl、directx 等,在 linux 中我推荐试试 SDL,接口比较友好)自行绘制。如果是嵌入式板子,需要交叉编译 SDL 等库。

四、如果只能使用 QPainter 下的优化方法

如果嵌入式板子没有 GPU,不支持 OpenGL,只能用 CPU 绘图的话,且由于技术原因或个人喜好等等不想使用第三方库的话,只能使用 QPainter,有下面几种优化方法,尤其是双缓冲机制和多线程渲染。


4.1 利用双缓冲机制绘图

方法是先将要绘制的内容绘制在一个图片中,再将图片一次性地绘制到控件上,而不是在绘图事件中进行渲染处理。这里不再赘述,具体请看我以前写的博客:


4.2 多线程渲染后更新绘图

可以采用多线程绘制在图像内存,然后通过信号发送 QPixmap,然后在 UI 主线程绘图。


4.3 拆分绘图数据到哈希表,以空间换时间

这里不再赘述,具体请看博客:QT 2d绘图优化(一)


4.4 使用 QPainterPath 优化

批处理所有相同类型的绘画调用,其中一种类型使用相同的画笔和笔。为此,可以使用类 QPainterPath 并使用 addRect 等函数添加所需的对象。确保在绘画功能之外执行此操作!具体可以参考:c++ - QT 5.0中疯狂的CPU使用率


4.5 定义绘制区域

我们只需要把少部分的区域绘制出来,不用绘制整个图标的数据,所以需要定义一个 viewport。


4.6优化绘图指令
  • 裁剪 :将不需要绘制的数据在渲染前裁剪掉
  • 局部更新:没有变动的地方不渲染
  • 分割缓存:分层或分成多个对象,总之将多变元素和少变元素分开,并缓存少变元素


4.7 其它注意事项
  • 显示视频的那个类,设置父类为 nullptr,这样,CPU 占用率会下降一些;

  • QPainter 要设置为 smooth 模式;

  • 最好选择 QWidget 的绘图事件绘图,而不是使用 QLabel 绘图,QLabel 只适合显示单张图片;

  • 如果绘制多张图片,也最好使用 Qidget 的绘图事件绘图,而不是用 QLabel 的 setPixmap;

  • QImage 到 QPixmap 转换耗费的资源较多。将 QImage 在 QLabel 显示容易卡死;

  • 解码后的数据,如果是通过 memcpy 放到缓存中的话,会比较消耗 CPU。

五、总结

Qt 如果需要高频绘制视频的话,根据嵌入式板子是否有 GPU 来进行方案选择:

  • 如果嵌入式板子有 GPU,则使用 EGLFS 平台插件,最推荐使用 Qt 官方自带的 QOpenGLWidget 来绘图;
  • 如果没有 GPU,一般只能使用 LinuxFB 平台插件,推荐使用 SDL、OpenCV 等第三方库;
  • 如果不想使用第三方库的话,那只能使用 QPainter 实现绘图了,就要注意优化了,否则 CPU 占用率很容易偏高。

四、优化界面刷新

现象
项目中有一项功能是在子线程里读数据,数据若发生变化了就发送信号,在槽里更新界面。程序运行的时候会出现数据变化很频繁的情况,也导致更新界面的信号在短时间内被触发很多次,大大损耗了程序的性能。

解决方法
因为人眼捕获的帧率有限,所以界面显示不需要太快的刷新速度,把信号槽更新界面改为用定时器更新界面,就能避免某一时间段数据变化过频导致的卡顿问题。

五、样式表导致绘图事件频繁调用

现象

QWidget:主窗口,背景利用setStyleSheet设置,同时重写paintEvent事件(QWigdet 的 paintEvent 默认为空):

void mainWidget::paintEvent(QPaintEvent *event)
{
    QStylePainter painter(this);
    QStyleOption opt;
    opt.initFrom(this);
    opt.rect = this->rect();
    painter.drawPrimitive(QStyle::PE_Widget,opt);
    QWidget::paintEvent(event);
}

主窗口上有众多不规则按键(某些按键组合成按键组,按下一个按键按键组的外观一起改变,通过设置样式表实现),每个按键都setMask设置有效区域,按键在按下,松开,选中都会有不同的样式,通过setStyleSheet设置。

结果发现,按键在响应过程中非常慢,top 看一下 CPU 占用率在点击按键时达到了 90%+,重绘事件在控件外观发生变化时会被调用。

猜测:在操作按键(按下,松开,点击)时在其槽函数改变按键的样式表,按键为主窗口的子控件。所以也会操作到主窗口的样式表。也即,主窗口的重绘事件也被调用。也就导致了每点击一个按键,最少也要调用三次主窗口的重绘事件。重复多次调用重绘事件使得在嵌入式平台中响应速度变慢。

解决方法

主窗口不使用样式表方式设置背景,改用调色板方式,此时主窗口样式表为空:

mainWidget::mainWidget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::mainWidget)
{
    QPixmap pixmap;
    QPalette palette;
    setWindowFlags(Qt::FramelessWindowHint);
    pixmap.load(":/images/res/mianbackground.png");
    palette.setBrush(QPalette::Window, QBrush(pixmap));
    setPalette(palette);
    ui->setupUi(this);
···
}

此时操作按键,CPU 占用率已经大大减小了,基本不会有超过 10% 的情况。

六、短时间内创建大量控件

现象

如果短时间内创建多个控件,例如一个 10x10 的控件网格,测试发现也会比较消耗 CPU 资源,尤其这个自定义控件本身比较消耗资源的话。

解决方法

  • 优化自定义控件,减少单个控件创建所需的资源;
  • 每创建一个或者一部分控件后,加点 sleep() 延时,但是这样会导致创建控件比较慢,视觉效果也不是很好,看怎么取舍了;
  • 如果是创建大量只需要显示的控件,例如一些图标控件,可以考虑使用 Qt 的图形视图 Graphics View 框架,创建图元比创建 QWidegt 控件快多了,但是需要考虑 QWidget 与 QGraphicsScene 的嵌套使用。

七、短时间内多次调用 qDebug() 打印造成卡顿

现象

如果执行了一个操作,然后短时间内多次调用 qDebug() 打印,测试发现也会比较消耗 CPU 资源。

解决方法

所以平时不要加太多打印,正式发布后一些调试打印要即使删除。如果为了测试方便,有些打印不想删除,可以选择正式版本关闭打印,测试时可以使用终端执行./proName d来运行应用程序以输出打印,d是要输出打印的后缀。代码如下:

// 默认先关闭qDebug()打印,以减少短时间内输出大量qDebug()打印造成的卡顿
QLoggingCategory::defaultCategory()->setEnabled(QtDebugMsg, false);
// 如果执行./proName d,则开放打印
if(QString::fromUtf8(argv[1]) == "d") {
	printf("Open qDebug!!!!!\n");
	QLoggingCategory::defaultCategory()->setEnabled(QtDebugMsg, true);
}

八、设置线程优先级

现象
项目中有多线程的操作,所有的线程都是用默认的QThread::InheritPriority优先级。因为设备的性能有限,让所有的线程都以高优先级去运行,主线程被过多地占用资源,导致界面刷新卡顿的问题。

解决方法
Qt 的线程提供了以下几种优先级(由低到高排列),按照实际情况去设置优先级,可以提高界面的流畅度。

  • QThread::IdlePriority
  • QThread::LowestPriority
  • QThread::LowPriority
  • QThread::NormalPriority
  • QThread::HighPriority
  • QThread::HighestPriority
  • QThread::TimeCriticalPriority
  • QThread::InheritPriority

九、检查子线程的延时操作

现象
项目里有一个子线程的代码结构是这样的。

void run() {
	while (true) {
		if (...) {
   			continue;
		}
	
		..... //operate
		msleep(20);
	}
}

线程在延时状态下是会把 CPU 交给其它线程的,避免不停地抢占 CPU,以上代码虽然加了延时,但前面有一个判断操作,如果满足了条件,就直接调用continue,如果这个判断条件一直都是满足的,那就会不停地 continue,代码也就变成了以下形式。

void run() {
	while (true) {
   		continue;
	}
}

等同于一个死循环,这样会大大影响程序的效率。

解决方法
在每个 continue 前添加延时。

void run() {
	while (true) {
		if (...) {
			msleep(20);
   			continue;
		}
	
		..... //operate
		msleep(20);
	}
}

或者避免使用 continue:

void run() {
	while (true) {
		if (...) {
			..... //operate
		}
	
		msleep(20);
	}
}

另外多线程,要合理的使用 sleep。对于while(1)中有耗时处理的,添加Sleep(0)就可以;对于没有耗时处理的,添加Sleep(0)就不行了,Sleep(1)Sleep(5)都行。

十、优化数据库查询方式

如果表中数据量大或者查询频率高,可以使用 map 容器来存放,以空间换时间,减少查询损耗。或者调整数据结构,将 2,3 百条数据的数组改成 map 结构储存等等。

十一、通信协议的优化

可以通过优化通信协议来减少客户端和服务端的计算量。

十二、减少不必要的IO操作

IO 操作是比较耗时的,常见的 IO 有通信 IO 和文件 IO,可以检查相关代码进行优化。

十三、使用VS性能分析工具

具体使用可参考:Qt项目使用VS的性能分析工具

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

高亚奇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值