【问题标题】:What is the common idiom for resizing a widget with a large image in Qt?在 Qt 中使用大图像调整小部件大小的常见习语是什么?
【发布时间】:2016-10-18 14:45:51
【问题描述】:

这看起来像是过早的优化,但我想了解内部发生了什么,以及这通常是如何使用 Qt 库进行编程的。

想象一个应用程序不断地生成一个填满整个窗口的图像,例如3D 实时渲染器。 (照片编辑器似乎没有这个问题,因为它旨在保留输出图像的大小,而不是在图像不适合时添加滚动条。)显然,当窗口调整大小时,输出(缓冲区)图像应该调整大小.

现在,在 Qt 中,似乎无法调整 QImage 的大小,而是必须释放当前图像并分配新图像。分辨率为 1280x1024 和 3 个 8 位通道的图像占用 3.75 Mb。调整大小事件确实经常到达(我已经测试过了),即窗口移动角的每隔几个像素(在 64 位 Linux 下的 X11 上使用 Qt5)。因此,问题:

  • 在现代台式机 CPU 上(考虑到整个平台,即 RAM、总线和其他方面),每秒重新分配几次 Mb 是否有任何重大负载?
  • 在上述那种平台上,当重新分配发生时,如果可能的话,它是发生在缓存中还是在 RAM 中?
  • Qt 中处理此类问题的常用习语是什么?有事件压缩,但即使应用了它,事件也会每秒到达几次(见介绍)。使用单次QTimer 超时在 100-200 毫秒范围内等待调整大小事件停止流动是一个好主意吗?

熟悉“机器会很好地处理它”的可能答案,但如果我这样对待它,我会认为自己是一个文盲的程序员。不管今天的 CPU 有多强大,我都想了解它是如何工作的。

【问题讨论】:

  • 如今,使用 Qt5+ 和 Qt Quick 2.0 的高性能 Qt 应用程序将在 GPU 上通过 QSG 和 OpenGL 完成任何交互式图像大小调整,这对于单纯的图像重采样具有足够的性能。旧的 Qt4 时代基于 QPainter 的 QtQuick 确实更受 CPU 性能的限制(例如,您曾经看到在动画期间禁用“平滑”的建议;将 qt5 的 doc.qt.io/qt-5/qquickitem.html#smooth-prop 与 qt4 的 doc.qt.io/qt-4.8/qdeclarativeitem.html#smooth-prop 进行比较)。

标签: c++ qt optimization memory-management allocation


【解决方案1】:

在现代台式机 CPU 上(考虑到整个平台,即 RAM、总线和其他方面),每秒重新分配几次 Mb 是否有任何重大负载?

在典型的现代分配器中,一次分配的成本是固定的,并且与“小”分配的分配大小无关。对于较大的分配,分配大小为 O(N),比例常数非常低。

顶级 Qt 小部件由 QImage 缓冲区或 OpenGL 上下文(如果您使用 QOpenGLWidget)支持。窗口支持缓冲区的大小调整由 Qt 自动处理——它已经发生了,你甚至没有注意到它!这没什么大不了的,就性能而言。现代分配器并不笨,不会对堆进行分段。

在上面描述的那种平台上,当重新分配发生时,如果可能的话,它是发生在缓存中还是在 RAM 中?

这没关系,因为无论如何你都会覆盖它。当然,如果有可用的缓存线会有所帮助,并且为对象重用相同的地址会有所帮助。

Qt中处理这类问题的常用习语是什么?

  1. 有一个槽用于更新要显示的数据(例如更新图像或某些参数),并调用QWidget::update()

  2. 在paintEvent中渲染它。

其余的会自动发生。 paintEvent 需要多长时间并不重要——如果需要很长时间,UI 的响应能力将会下降,但它永远不会尝试显示过时的数据。没有事件的累积。


图像缩放通常由QImage::scaled 处理,返回一个临时图像,然后您使用QPainter::drawImage 绘制该图像。是的,那里有分配,但这些分配很快。

图像制作者的事件风暴很容易解决:当有新图像可用时,制作者会发出信号。图像消费者有一个槽,它接受图像,将其复制到内部成员,并触发更新。更新在控件返回事件循环时生效,并使用最近设置的图像。当没有其他事件要处理时,重绘将继续进行,因此无论需要多长时间:它总是会显示最新的图像。它永远不会“滞后”。

很容易验证这种行为。在下面的示例中,ImageSource 尽可能快地生成新帧(大约 1kHz)。每帧显示当前时间。 Viewer 在其 paintEvent 中休眠,将屏幕刷新率限制在 4Hz 以下:除非您在严重过热的内核上运行,否则它在现实生活中永远不会那么慢。每次屏幕刷新至少有 25 个新帧。然而,您在屏幕上看到的时间是当前时间。过期的帧会被自动丢弃。

// https://github.com/KubaO/stackoverflown/tree/master/questions/update-storm-image-40111359
#include <QtWidgets>

class ImageSource : public QObject {
  Q_OBJECT
  QImage m_frame{640, 480, QImage::Format_ARGB32_Premultiplied};
  QBasicTimer m_timer;
  double m_period{};
  void timerEvent(QTimerEvent * event) override {
    if (event->timerId() != m_timer.timerId()) return;
    m_frame.fill(Qt::blue);
    QElapsedTimer t;
    t.start();
    QPainter p{&m_frame};
    p.setFont({"Helvetica", 48});
    p.setPen(Qt::white);
    p.drawText(m_frame.rect(), Qt::AlignCenter,
               QStringLiteral("Hello,\nWorld!\n%1").arg(
                 QTime::currentTime().toString(QStringLiteral("hh:mm:ss.zzz"))));
    auto const alpha = 0.001;
    m_period = (1.-alpha)*m_period + alpha*(t.nsecsElapsed()*1E-9);
    emit newFrame(m_frame, m_period);
  }
public:
  ImageSource() {
    m_timer.start(0, this);
  }
  Q_SIGNAL void newFrame(const QImage &, double period);
};

class Viewer : public QWidget {
  Q_OBJECT
  double m_framePeriod;
  QImage m_image;
  QImage m_scaledImage;
  void paintEvent(QPaintEvent *) override {
    qDebug() << "Waiting events" << d_ptr->postedEvents;
    QPainter p{this};
    if (m_image.isNull()) return;
    if (m_scaledImage.isNull() || m_scaledImage.size() != size())
      m_scaledImage = m_image.scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
    p.drawImage(0, 0, m_scaledImage);
    p.drawText(rect(), Qt::AlignTop | Qt::AlignLeft, QStringLiteral("%1 FPS").arg(1./m_framePeriod));
    if (true) QThread::msleep(250);
  }
public:
  Q_SLOT void setImage(const QImage & image, double period) {
    Q_ASSERT(QThread::currentThread() == thread());
    m_image = image;
    m_scaledImage = {};
    m_framePeriod = period;
    update();
  }
};

class Thread final : public QThread { public: ~Thread() { quit(); wait(); } };

int main(int argc, char ** argv) {
  QApplication app{argc, argv};
  Viewer viewer;
  viewer.setMinimumSize(200, 200);
  ImageSource source;
  Thread thread;
  QObject::connect(&source, &ImageSource::newFrame, &viewer, &Viewer::setImage);
  QObject::connect(&thread, &QThread::destroyed, [&]{ source.moveToThread(app.thread()); });
  source.moveToThread(&thread);
  thread.start();
  viewer.show();
  return app.exec();
}
#include "main.moc"

将图像缩放分担到 GPU 通常是有意义的。 This answer 提供了完整的解决方案。

【讨论】:

  • 很好的答案,它证实了我从一开始就采用了通知语义的正确方式。我创建了一个带有 2 个标志和一个计时器的预览渲染机制来处理消费者端的请求队列,因为帧需要大量时间来渲染并且 UI 中不欢迎停顿。尚未运行代码,但我肯定感谢您的努力!
  • @iksemyonov 生产者和消费者应该可能不知道对方。标志阻碍了这一点 - 除了零持续时间计时器之外,您既不需要标志也不需要计时器。
  • 在我的情况下,需要一个计时器来在预览渲染后不久自动启动全分辨率渲染,所以它不是您可能认为的计时器:)
  • 不使用计时器也可以做到这一点,对吧?尽快完成全分辨率渲染?还是有一些要求排除了这一点? (我试图了解发生了什么)
  • 这是为了互动。首先是缩小版本,然后当我例如旋转或缩放相机,渲染器仍然输出缩小,因为它是 1-2 秒 vs 0.2 秒。然后,当用户输入停止时,程序可以全分辨率渲染。不过没这么想,也许我真的可以去掉定时器,以后试试看!
猜你喜欢
  • 1970-01-01
  • 2012-10-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多