【问题标题】:Does large use of signals and slots affect application performance?大量使用信号和插槽会影响应用程序性能吗?
【发布时间】:2024-01-17 19:39:01
【问题描述】:

这个问题只是为了教育目的:

在两个对象(例如两个线程)之间使用 30-50 或更多对信号和插槽是否会影响应用程序性能、运行时或响应时间?

【问题讨论】:

    标签: qt signals-slots


    【解决方案1】:

    首先,您应该在 QThreads 中放置任何插槽。除了重新实现run 方法和私有方法(不是信号!)之外,QThreads 并不是真的要派生自其他方法。

    QThread 在概念上是一个线程控制器,而不是线程本身。在大多数情况下,您应该处理 QObjects。启动一个线程,然后将对象实例移动到该线程。这是使插槽在线程中正常工作的唯一方法。将线程实例(它 是 QObject 派生的!)移动到线程是一种 hack 和糟糕的风格。尽管不知情的论坛帖子另有说明,但不要这样做。

    至于您的其余问题:信号槽调用不必定位任何内容,也不必进行太多验证。 “定位”和“验证”在连接建立时完成。通话时完成的主要步骤是:

    1. 从池中锁定一个信号槽互斥体。

    2. 遍历连接列表。

    3. 使用直接呼叫或排队呼叫执行呼叫。

    共同成本

    任何信号槽调用总是从 moc 生成的信号实现中的直接调用开始。在堆栈上构造一个指向信号参数的指针数组。参数不会被复制。

    信号然后调用QMetaObject::activate,其中获取连接列表互斥体,并迭代连接的槽列表,为每个槽放置调用。

    直接连接

    这里没有做太多,通过直接调用在建立连接时获得的QObject::qt_static_metacallQObject::qt_metacall 来调用插槽,如果QMetaObject::connect 用于设置连接。后者允许dynamic creation of signals and slots

    排队连接

    参数必须编组和复制,因为调用必须存储在事件队列中并且信号必须返回。这是通过分配一个指向副本的指针数组并复制堆上的每个参数来完成的。执行此操作的代码实际上是简洁的旧 C。

    呼叫的排队在queued_activate 内完成。这是复制构造完成的地方。

    排队调用的开销始终是至少一个QMetaCallEvent 的堆分配。如果调用有任何参数,则分配一个指向参数的数组,并为每个参数进行额外的分配。对于带有n 参数的调用,作为C 表达式给出的成本是(n ? 2+n : 1) 分配。阻塞调用的返回值是作为参数的计数器。可以说,Qt 的这一方面可以优化为对所有内容进行一次分配,但在现实生活中,只有调用普通方法才有意义。

    基准测试结果

    即使是直接(非排队)信号槽调用也有可衡量的开销,但您必须选择战斗。易于构建代码与性能。您确实测量了最终应用程序的性能并确定了瓶颈,对吗?如果这样做,您可能会发现在实际应用中,信号槽开销不起作用。

    唯一具有显着开销的时间信号槽机制是调用普通函数。假设您在下面的代码中调用 trivial 插槽。这是一个完整的、独立的基准测试,所以请随意运行它并亲自查看。我机器上的结果是:

    Warming up the caches...
    trivial direct call took 3ms
    nonTrivial direct call took 376ms
    trivial direct signal-slot call took 158ms, 5166% longer than direct call.
    nonTrivial direct signal-slot call took 548ms, 45% longer than direct call.
    trivial queued signal-slot call took 2474ms, 1465% longer than direct signal-slot and 82366% longer than direct call.
    nonTrivial queued signal-slot call took 2474ms, 416% longer than direct signal-slot and 653% longer than direct call.
    

    也许应该注意的是,连接字符串非常快:)

    请注意,我是通过函数指针进行调用,这是为了防止编译器优化对加法函数的直接调用。

    //main.cpp
    #include <cstdio>
    #include <QCoreApplication>
    #include <QObject>
    #include <QTimer>
    #include <QElapsedTimer>
    #include <QTextStream>
    
    static const int n = 1000000;
    
    class Test : public QObject
    {
        Q_OBJECT
    public slots:
        void trivial(int*, int, int);
        void nonTrivial(QString*, const QString&, const QString&);
    signals:
        void trivialSignalD(int*, int, int);
        void nonTrivialSignalD(QString*, const QString&, const QString &);
        void trivialSignalQ(int*, int, int);
        void nonTrivialSignalQ(QString*, const QString&, const QString &);
    private slots:
        void run();
    private:
        void benchmark(bool timed);
        void testTrivial(void (Test::*)(int*,int,int));
        void testNonTrivial(void (Test::*)(QString*,const QString&, const QString&));
    public:
        Test();
    };
    
    Test::Test()
    {
        connect(this, SIGNAL(trivialSignalD(int*,int,int)),
                SLOT(trivial(int*,int,int)), Qt::DirectConnection);
        connect(this, SIGNAL(nonTrivialSignalD(QString*,QString,QString)),
                SLOT(nonTrivial(QString*,QString,QString)), Qt::DirectConnection);
        connect(this, SIGNAL(trivialSignalQ(int*,int,int)),
                SLOT(trivial(int*,int,int)), Qt::QueuedConnection);
        connect(this, SIGNAL(nonTrivialSignalQ(QString*,QString,QString)),
                SLOT(nonTrivial(QString*,QString,QString)), Qt::QueuedConnection);
        QTimer::singleShot(100, this, SLOT(run()));
    }
    
    void Test::run()
    {
        // warm up the caches
        benchmark(false);
        // do the benchmark
        benchmark(true);
    }
    
    void Test::trivial(int * c, int a, int b)
    {
        *c = a + b;
    }
    
    void Test::nonTrivial(QString * c, const QString & a, const QString & b)
    {
        *c = a + b;
    }
    
    void Test::testTrivial(void (Test::* method)(int*,int,int))
    {
        static int c;
        int a = 1, b = 2;
        for (int i = 0; i < n; ++i) {
            (this->*method)(&c, a, b);
        }
    }
    
    void Test::testNonTrivial(void (Test::* method)(QString*, const QString&, const QString&))
    {
        static QString c;
        QString a(500, 'a');
        QString b(500, 'b');
        for (int i = 0; i < n; ++i) {
            (this->*method)(&c, a, b);
        }
    }
    
    static int pct(int a, int b)
    {
        return (100.0*a/b) - 100.0;
    }
    
    void Test::benchmark(bool timed)
    {
        const QEventLoop::ProcessEventsFlags evFlags =
                QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers;
        QTextStream out(stdout);
        QElapsedTimer timer;
        quint64 t, nt, td, ntd, ts, nts;
    
        if (!timed) out << "Warming up the caches..." << endl;
    
        timer.start();
        testTrivial(&Test::trivial);
        t = timer.elapsed();
        if (timed) out << "trivial direct call took " << t << "ms" << endl;
    
        timer.start();
        testNonTrivial(&Test::nonTrivial);
        nt = timer.elapsed();
        if (timed) out << "nonTrivial direct call took " << nt << "ms" << endl;
    
        QCoreApplication::processEvents(evFlags);
    
        timer.start();
        testTrivial(&Test::trivialSignalD);
        QCoreApplication::processEvents(evFlags);
        td = timer.elapsed();
        if (timed) {
            out << "trivial direct signal-slot call took " << td << "ms, "
                   << pct(td, t) << "% longer than direct call." << endl;
        }
    
        timer.start();
        testNonTrivial(&Test::nonTrivialSignalD);
        QCoreApplication::processEvents(evFlags);
        ntd = timer.elapsed();
        if (timed) {
            out << "nonTrivial direct signal-slot call took " << ntd << "ms, "
                   << pct(ntd, nt) << "% longer than direct call." << endl;
        }
    
        timer.start();
        testTrivial(&Test::trivialSignalQ);
        QCoreApplication::processEvents(evFlags);
        ts = timer.elapsed();
        if (timed) {
            out << "trivial queued signal-slot call took " << ts << "ms, "
                   << pct(ts, td) << "% longer than direct signal-slot and "
                   << pct(ts, t) << "% longer than direct call." << endl;
        }
    
        timer.start();
        testNonTrivial(&Test::nonTrivialSignalQ);
        QCoreApplication::processEvents(evFlags);
        nts = timer.elapsed();
        if (timed) {
            out << "nonTrivial queued signal-slot call took " << nts << "ms, "
                   << pct(nts, ntd) << "% longer than direct signal-slot and "
                   << pct(nts, nt) << "% longer than direct call." << endl;
        }
    }
    
    int main(int argc, char *argv[])
    {
        QCoreApplication a(argc, argv);
        Test t;
        return a.exec();
    }
    
    #include "main.moc"
    

    【讨论】:

    • 我建议看一下关于如何真正明确使用 QThread mayaposch.wordpress.com/2011/11/01/… 的非常清晰的教程
    • 上面的代码有一个bug,他最后一次测试打印了上面测试的经过时间(ts)。
    【解决方案2】:

    当然它们会影响应用程序的性能,主要是因为定位连接对象+验证槽对象状态 n 所花费的时间。但是信号和槽机制的简单性和灵活性非常值得开销。Plus one of the major advantage of signal-slot mechanism is they are type=safe allowing communication between objects, irrespective of type of object unlike callbacks.

    与回调相比,信号和槽的速度稍慢,因为它们提供了更高的灵活性,尽管对于实际应用程序的差异并不显着。一般来说,发出一个连接到某些槽的信号,比直接调用接收器慢大约十倍,使用非虚拟函数调用。这是定位连接对象、安全地迭代所有连接(即检查后续接收器在发射期间是否被破坏)以及以通用方式编组任何参数所需的开销。例如,虽然十个非虚拟函数调用听起来很多,但它的开销比任何新操作或删除操作要少得多。一旦你执行了一个字符串、向量或列表操作,在后台需要new或delete,信号和槽的开销只负责一个很小的 占全部函数调用成本的比例。

    来源:Signals and Slots

    【讨论】:

    • 您确定开销系数有那么低吗?信号调用 QMetaObject::activate,它有大约一百行代码。我猜它比插槽的直接非虚拟调用慢大约 100 倍。但我同意你的看法:在大多数情况下,这种开销是微不足道的。
    • 单线程应用程序中现代内存分配器上的新操作或删除操作的开销很小。很小。实际上是如此之小,以至于将两个 1000 个字符的 QString 连接成一个新的 QString 所花费的时间与直接信号槽连接开销一样多!
    • 对于快速实时应用程序,我们是否会说信号和插槽应该不受欢迎?例如。在嵌入式系统中,以大约 5ms 的间隔将数据从串行端口读取到 C++ 数据结构中
    最近更新 更多