Почему мой сигнальный слот медленнее, чем QThreadPool+new+delete?
Я читал в Qt Signals & Slots [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+ сигнал / слоты всегда отставали. При большем количестве заданий QThreadPool+new/delete может легко превзойти QThread в 10 раз.
Я чувствую себя неловко из-за своего кода. Я что-то здесь неправильно понял? Если сигнал / слот быстрее, чем новый / удалить, в чем проблема с моим тестом?
Спасибо.
1 ответ
Существует различие в производительности сигнала в зависимости от типа подключения. Когда вы создаете соединения между потоками, соединение ставится в очередь и использует цикл событий для планирования самого себя, а цикл событий в 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 раз медленнее, чем прямое. И я подозреваю, что документация, на которую вы ссылаетесь, относится к прямой связи.
В общем, я бы сказал, что ваш тест - полный беспорядок. Вы должны действительно упростить это, сделать это простым и сосредоточиться на проблеме, которую вы поднимаете.
Наконец, прямые соединения могут быть быстрее, чем новые / удалять, но соединения с очередями, скорее всего, нет, они намного медленнее и, безусловно, являются ключевым фактором, влияющим на изменяющуюся производительность. Заявление, содержащееся в документации, на которую вы ссылаетесь, не имеет абсолютно никакого отношения к QThread + worker
против QRunnable + thread pool
спектакль. И, наконец, в обоих случаях вы используете как динамическое выделение / освобождение памяти, так и соединения в очереди.