測試環境
- VirtualBox + Ubuntu 12.04
- CPU: Intel i5 3.2GHz 4 cores
- Hard Drive: 256G SSD
- Main Memory: 4G
- Qt: 4.8.5
測試 1. Qt::DirectConnection
//sender.h #ifndef SENDER_H #define SENDER_H #include <QObject> class Sender : public QObject { Q_OBJECT public: explicit Sender(QObject *parent = 0); void run(uint val); signals: void mysignal(uint val); public slots: }; #endif // SENDER_H
//sender.cpp #include "sender.h" Sender::Sender(QObject *parent) : QObject(parent) { } void Sender::run(uint val) { emit mysignal(val); }
//receiver.h #ifndef RECEIVER_H #define RECEIVER_H #include <QObject> class Receiver : public QObject { Q_OBJECT public: explicit Receiver(QObject *parent = 0); signals: public slots: void myslot(uint val); private: uint total_; }; #endif // RECEIVER_H
//receiver.cpp #include "receiver.h" Receiver::Receiver(QObject *parent) : QObject(parent), total_(0) { } void Receiver::myslot(uint val) { total_ += val; }
//main.cpp #include <QtCore/QCoreApplication> #include <QDateTime> #include <QTimer> #include "sender.h" #include "receiver.h" int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); Sender s; Receiver r; QObject::connect(&s, SIGNAL(mysignal(uint)), &r, SLOT(myslot(uint))); quint64 before = QDateTime::currentMSecsSinceEpoch(); for(uint i = 0; i < 100000000; ++i) s.run(i); qDebug("%u ms", uint(QDateTime::currentMSecsSinceEpoch()-before)); QTimer::singleShot(0, &app, SLOT(quit())); return app.exec(); }emit signal 一億次的結果是需要 7.x 秒左右。
測試 2. Qt::QueuedConnection
因為 sender.h/cpp, receiver.h/cpp 與測試 1 相同,此處不再重複。另外下面的程式有點問題,不知道您看出來了嗎?
//testthread.h #ifndef TESTTHREAD_H #define TESTTHREAD_H #include <QThread> #include "sender.h" class Receiver; class TestThread : public QThread { Q_OBJECT public: explicit TestThread(Receiver *r, QObject *parent = 0); protected: void run(); signals: public slots: private: Sender snd_; }; #endif // TESTTHREAD_H
//testthread.cpp #include <QDateTime> #include "testthread.h" #include "receiver.h" TestThread::TestThread(Receiver *r, QObject *parent) : QThread(parent) { snd_.moveToThread(this); connect(&snd_, SIGNAL(mysignal(uint)), r, SLOT(myslot(uint))); } void TestThread::run() { quint64 before = QDateTime::currentMSecsSinceEpoch(); for(uint i = 0; i < 100000000; ++i) snd_.run(i); qDebug("%u ms", uint(QDateTime::currentMSecsSinceEpoch() - before)); }
//main.cpp #include <QtCore/QCoreApplication> #include <QDateTime> #include <QTimer> #include "sender.h" #include "receiver.h" #include "testthread.h" int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); Receiver r; TestThread tt(&r); tt.start(); return app.exec(); }結果要花上 11x-14x 秒,也就是比測試 1 慢上 15-20 倍!如果您去追蹤 Qt source code,您會發現他的 message loop 使用 select()/epoll() 傳遞訊息,也就是必須在 user space <-> kernel space 間來回穿梭,外加 thread context switch overhead。
如果以 Heresy 兄的比較為基準,在他的測試結果中 ,Qt signal/slot 比一般 function member 慢了 75 倍,inter-thread signal/slot 就是慢了 1125-1500 倍(15 * 75 = 1125, 20 * 75 = 1500)!
在 x86 CPU 上可能還無所謂,但在 Embedded Linux 上就是很可怕的開銷,老闆會開始抱怨為何 CPU 要從 ARM9 400MHz 換成 Cortex A8!?
我們再往下看,Qt 在什麼使用場景下會產生大量的 signal。
當 Qt 遇上通訊
有人的做法是用一個 thread polling 網路上的訊息,然後用 emit signal 傳給 GUI thread 更新畫面。這樣做會有幾個問題,除了上面的範例程式已經告訴你,光是把一個 32bit 整數由一個 thread 的 signal 傳給另外一個 thread 的 slot,代價是 local signal/slot 的 10-15 倍。另外兩個問題是:
- thread context switch 時機點無法預測
- 網路通訊延遲
這樣做,很難保畫面更新不會「怪怪的」
而且由於 Linux Qt 底層走的是 select()/epoll(),他也不是最快的 IPC,要知道既然是 multi-threading,那代表兩個 thread 共享位址空間。另外,為了保證能準時更新畫面,用 timer 才是最保險的,所以改成下面的架構是否合理多了?(下圖的 shared memory 與常見的 UNIX IPC shmem 不是同一個東西,因為同一個 process 內的兩個 thread 本來就能共享位址空間)。
(其實,上圖的架構在畫面更新的正確作法一文已經提過了...)
當然,有人會說幹麼不用 Qt 本身提供的網路功能就好了?這有一些理由,比方說 Qt 5 之前不支援 serial port,或者是很多人已經很習慣 per thread + blocking IO 的作法了(以筆者碰到的例子是因為前人留下的包袱),也有可能你是整合非 qt 架構的 source 進入專案。
不過這也是很值得討論的方向。究竟 Qt 有沒有用到該平台上效能最佳的 IO 模型,例如 Windows IO Completion Port,Linux epoll?
沒有留言:
張貼留言