为什么我的信号槽比QThreadPool+new+delete慢?

3

我正在阅读Qt的信号和槽[1],注意到它声称信号和槽的开销比任何新建或删除操作都要低。于是我进行了一次测试:

#include <cmath>

#include <QtCore/QAtomicInt>
#include <QtCore/QCoreApplication>
#include <QtCore/QElapsedTimer>
#include <QtCore/QMetaObject>
#include <QtCore/QMetaMethod>
#include <QtCore/QObject>
#include <QtCore/QRunnable>
#include <QtCore/QTextStream>
#include <QtCore/QThread>
#include <QtCore/QThreadPool>
#include <QtCore/QTimer>
#include <QtCore/QVector>

using std::pow;

constexpr int const maxThreadCount(16);
constexpr int const maxIteration(100000);
constexpr int const maxPiDigit(1000);

void calcPi()
{
    double sum(0);
    for (int k(0); k < maxPiDigit; ++k) {
        double a(4.0 / (k * 8 + 1));
        double b(2.0 / (k * 8 + 4));
        double c(1.0 / (k * 8 + 5));
        double d(1.0 / (k * 8 + 6));
        sum += pow(16, -k) * (a - b - c -d);
    }
    QTextStream out(stdout);
    out << sum << endl;
}

class CalcPiWithQObject : public QObject
{
    Q_OBJECT

    public:
        CalcPiWithQObject(QObject *parent = NULL);

    public slots:
        void start();

    signals:
        void finished();
}; // CalcPiWithQObject

CalcPiWithQObject::CalcPiWithQObject(QObject *parent):
    QObject(parent)
{}

void CalcPiWithQObject::start()
{
    calcPi();
    finished();
}

class CalcPiWithQRunnable : public QRunnable
{
    private:
        static QAtomicInt count_;

    public:
        CalcPiWithQRunnable(QThreadPool *parent);

        void run() override;

    private:
        QThreadPool *parent_;
}; // CalcPiWithQRunnable

QAtomicInt CalcPiWithQRunnable::count_(maxThreadCount);

CalcPiWithQRunnable::CalcPiWithQRunnable(QThreadPool *parent):
    QRunnable(),
    parent_(parent)
{
    setAutoDelete(false);
}

void CalcPiWithQRunnable::run()
{
    calcPi();
    if (count_.fetchAndAddOrdered(1) < maxIteration) {
        parent_->start(new CalcPiWithQRunnable(parent_));
    }
    delete this;
}

class PiTest : public QObject
{
    Q_OBJECT

    public:
        PiTest(QObject *parent = NULL);

    public slots:
        void start();
        void nextQObjectCall();

    private:
        QVector<QThread *> threads_;
        QVector<CalcPiWithQObject *> calc_;
        QThreadPool *threadPool_;
        QElapsedTimer timer_;
        int threadCount_;
        int jobCount_;
}; // PiTest

PiTest::PiTest(QObject *parent):
    QObject(parent),
    threads_(maxThreadCount),
    calc_(maxThreadCount),
    threadPool_(new QThreadPool(this)),
    threadCount_(maxThreadCount),
    jobCount_(maxThreadCount)
{
    threadPool_->setMaxThreadCount(maxThreadCount);
    for (int i(0); i < maxThreadCount; ++i) {
        threads_[i] = new QThread();
        calc_[i] = new CalcPiWithQObject();
        calc_[i]->moveToThread(threads_[i]);
        QObject::connect(calc_[i], &CalcPiWithQObject::finished,
                         this, &PiTest::nextQObjectCall,
                         Qt::QueuedConnection);
        QObject::connect(threads_[i], &QThread::started,
                         calc_[i], &CalcPiWithQObject::start,
                         Qt::QueuedConnection);
    }
}

void PiTest::start()
{
    timer_.start();
    for (int i(0); i < maxThreadCount; ++i) {
        threadPool_->start(new CalcPiWithQRunnable(threadPool_));
    }
    threadPool_->waitForDone();
    int timePassed(timer_.elapsed());
    QTextStream out(stdout);
    out << "QThreadPool: " << timePassed << endl;
    timer_.restart();
    for (int i(0); i < maxThreadCount; ++i) {
        threads_[i]->start();
    }
}

static QMetaMethod nextCall(PiTest::staticMetaObject.method(PiTest::staticMetaObject.indexOfMethod("start")));

void PiTest::nextQObjectCall()
{
    jobCount_++;
    if (jobCount_ < maxIteration) {
        nextCall.invoke(sender(), Qt::QueuedConnection);
        QMetaObject::invokeMethod(sender(), "start",
                                  Qt::QueuedConnection);
        return;
    }
    threadCount_--;
    if (threadCount_ == 0) {
        for (int i(0); i < maxThreadCount; ++i) {
            threads_[i]->quit();
        }
        int timePassed(timer_.elapsed());
        QTextStream out(stdout);
        out << "QThread: " << timePassed << endl;
        qApp->quit();
    }
}

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    PiTest *bench(new PiTest(qApp));
    QTimer::singleShot(0, bench, SLOT(start()));
    return qApp->exec();
}

#include "main_moc.cpp"

我在一台空闲的20核电脑上运行了测试:

/usr/lib64/qt5/bin/moc -o main_moc.cpp main.cpp
clang++ -std=c++11 -fPIE -O2 -march=native -I/usr/include/qt5/ -L/usr/lib64/qt5 -lQt5Core -o bench main.cpp
./bench > test.out
grep QThread test.out

这里是结果:
QThreadPool: 4803
QThread: 9285

我尝试了不同的参数,延长圆周率计算时间和减少作业数量,或者反之亦然,但结果差不多。QThread+signal/slot 总是滞后于QThreadPool+new/delete。当作业数量较大时,QThreadPool+new/delete可以轻松地比QThread快上10倍。

关于我的基准代码,我感到有些尴尬。我是否在这里误解了什么?如果信号/插槽比新建/删除更快,那么我的基准测试有什么问题吗?

谢谢。

[1] http://doc.qt.io/qt-5/signalsandslots.html


@JosephMalicke,我想我正在测试信号槽。我可能在这里犯了错误。这就是为什么我要问这个问题的原因。目的是测试不同线程之间的信号/槽与new/delete。 - nocte107
我进行了一个测试,使用100个pi作业和10000000次迭代。这使得QThreadPool比QThread更糟糕。我不知道为什么。 - nocte107
1个回答

4
连接类型不同会影响信号传输效果。当您创建跨线程连接时,该连接将被排队,并使用事件循环进行调度。Qt中的事件循环非常缓慢,我上次检查时发现它没有提供任何增加更新速率的方法。
这使得跨线程的信号传输非常缓慢。我曾经遇到过一些细粒度并发的情况,多线程反而导致性能下降而不是提高性能。
为了让您了解直接连接和排队连接之间的区别,请看以下内容:
#define COUNT 5000
class Ping : public QObject {
  Q_OBJECT
  Q_SIGNAL void pong(uint);
public slots: void ping(uint c) { if (c < COUNT) emit pong(++c); else qDebug() << t.nsecsElapsed(); }
};

//...

QObject::connect(&p1, SIGNAL(pong(uint)), &p2, SLOT(ping(uint)), Qt::DirectConnection);
QObject::connect(&p2, SIGNAL(pong(uint)), &p1, SLOT(ping(uint)), Qt::DirectConnection);

//...

p1.ping(0);

结果:

Direct connection (in same thread)    - 570504 nsec
Queued connection (in same thread)    - 29670333 nsec
Queued connection (different threads) - 53343054 nsec

如您所见,线程之间的连接几乎比直接连接慢 100 倍。我怀疑您提供的文档是指直接连接。

总体而言,我认为您的测试是一团糟。您应该真正简化它,使其简单化并专注于您提出的问题。

最后,直接连接可能比new/delete更快,但排队连接绝对不是,它们要慢得多,并且绝对是性能变化的关键因素。您提供的文档声称与QThread + worker vs QRunnable + thread pool的性能无关,这完全没有任何关系。最后,在两种情况下,都使用了动态内存分配/释放排队连接


我觉得我在这里误解了。感谢澄清。我无法构建一个基准来将信号槽部分与新删除部分分离,因为我认为它们在这里是“不同的方法”。 - nocte107

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接