本文首發於 BriFuture 的 我的博客html
在個人前一篇文章 使用 Qt 獲取 UDP 數據並顯示成圖片 中,我講了如何用 Python 模擬發送數據,如何在 Qt 中高效的接收 UDP 數據包並將數據解析出來。然而此前的文章在分別顯示 RGB 通道、R 通道、G 通道、B 通道這四組通道的圖片時仍然會出現處理速度過慢的問題。python
前面說過編寫的程序至少會用到 3 個線程來分別處理 UI、socket 數據、數據解析,由於不這樣作無法在時限內處理完接收到的數據,寫第一篇博客的時候,我覺得是單純的使用 new 在堆中分配內存致使程序運行效率低,後來確實經過預分配對象內存解決了部分問題,可是還有一些會影響程序運行速度的問題沒有解決,也沒有深究,今天從新編寫代碼的時候,爲了分配數據到 4 幅圖片上(分別是 RGB 通道、R 通道、G 通道、B 通道),發現運行速度仍是不夠,影響運行速度的緣由有幾個:數組
接下來看看這幾個致使程序運行速度不夠的緣由:緩存
在 QtCreator 中運行程序,若是是以 Debug 模式運行的話,速度是要比 Release 模式低一些的。之前編寫 Qt 程序,數據量通常不大,對於性能都沒有要求,即便程序代碼不夠優化,但在用戶使用過程當中通常不會感覺到運行卡頓,因此一直都沒發現 Debug 模式和 Release 模式的性能有差別。多線程
不過其實也能猜到性能有差別的大概緣由:Debug 模式下會在最終生成的代碼裏面插入不少額外的代碼用於調試,可是 Release 生成的代碼是不會插入這些調試用的代碼的,最明顯的差別就是 Debug 模式生成的可執行文件比 Release 模式生成的可執行文件要大得多。app
Debug 模式下運行程序,實際 FPS 和指望的 FPS 有 6 幀的差距,差距產生的緣由是處理速度不夠,致使最終生成圖片的速度慢了。socket
Release 模式下即便是原始數據包的指望 FPS 到了 77 幀,實際的 FPS 也能夠達到 77 幀,也就是說在處理過程當中沒有出現處理速度跟不上接受數據的速度。函數
當咱們使用 Qt 程序的時候,常常會在主函數 main 裏寫出相似下面這樣的代碼:性能
int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow mw; mw.show(); return a.exec(); }
這樣咱們的程序應用的生命週期就是 QApplication 所定義的,當咱們使用 QObject::moveToThread 方法將某個 QObject (子類)對象移到其餘的子線程中的時候,子線程也有獨立於主線程的相應的事件派發機制。優化
QObject 的多線程使用方法很巧妙,利用信號&槽機制或者是 QMetaObject::invokeMethod 方法就可讓要執行的耗時函數在子線程中執行,可是若是是直接調用耗時函數,那麼就會在當前的線程中執行耗時操做,致使線程阻塞。
在線程之間傳遞數據,若是是用信號&槽的機制,那麼可能你都不須要考慮線程間的數據同步問題,但信號&槽機制是要依靠 Qt 的事件循環機制的,若是事件不能正常分發(dispatch),那麼子線程中的槽函數就不會被調用。
關於 Qt 的事件循環機制和線程機制,推薦看一看官方 wiki,《線程、事件與QObject》 或者也有對應的英文原文 Threads Events QObjects。
若是頻繁的調用信號,在 Qt 的事件循環中,由於前一次耗時的任務沒有完成,致使對應的槽函數沒法執行,最終致使處理速度跟不上。
所以對於實時性要求高的程序,Qt 的事件循環機制可能不會是你的首選,你更有可能去作的是在 Qt 的一個子線程中運行循環代碼,忽略掉該子線程中的事件循環以提升程序的性能。
在接收到原始字節數據以後,最重要也是最麻煩的就是解析數據。包括識別自定義協議數據的頭部信息,將數據包中的圖像數據複製到緩衝區,並將緩衝區中的數據以圖片的形式顯示出來。接下來分享幾個高效處理數據的幾個小技巧:
QByteArray data; data.resize(PacketSize); // PacketSize 是預先定義好的數據字節數 // 能夠簡單的認爲 rawData 就是從 UDP 端口中接收到的數據, // ValidDataSize 也是預先定義好的數據字段的長度 memcpy(rawData.data(), data.data(), ValidDataSize); // 上面的代碼要比直接使用賦值操做 = 高效 data = rawData;
LineDataObj
(用於表示圖片的一行數據)的時候,用 QMap 存儲行號和指針,利用 QMap 的查找功能減小了查找或排序的時間,可是缺點是 QMap 會隨着其內部的數據量增大變得緩慢,若是隻須要緩存數據,建議直接使用數組存取,這樣的運行效率最高。// 在類中聲明一個 map QMap<int line, LineDataObj *> map; // 在方法體中使用 map 查找是否有對應的行數據 if(map.contains(line)) { // 若是有對應的行數據對象,直接將數據寫入到行對象數據上 ... } else { // 若是沒有,則插入一條記錄 map.insert(line, lineDataObj); } // 處理完一行數據後,能夠將該行數據從 map 中移除掉 map.remove(line);
能夠發現,map 就是用來判斷是否有對應行數據對象,而後處理結束後移除保存的行號,這並無達到緩存數據的目的,反而再插入和移除的過程當中浪費了過多時間。但若是用一個數組當作緩存區就會快不少,由於咱們減小了查找和移除記錄的時間:
QVector<LineDataObj *> linePool; // LinePoolSize 是預約義的池大小 linePool.reserve(LinePoolSize); for(int i = 0; i < LinePoolSize; i++) { linePool.append(new LineDataObj()); } // 數組大小是有限的,行號倒是不斷增長的,所以要設置一個起始行,保證在長時間執行程序後不會出現數組越界的問題 int diffLine = line - startLine; // 進行處理 linePool[line].setData(...);
少便是多
的原則確實給了我很大的啓發)。我以前編寫程序時,除了有一個 LineDataObj
用來表示行對象,還有一個 RawDataObj
表示原始的數據包對象。處理的流程多:1. 接受原始數據包 => 2. 將數據包填充到 RawDataObj 中並解析數據包的行號,RGB 類型 => 3. 根據 RawDataObj 的屬性肯定對應的 LineDataObj => 4. 當 LineDataObj 存儲到必定數目時生成圖像。
這個流程很直觀也很容易想到,可是 RawDataObj 這個數據對象其實不必使用,由於它增長了一次沒必要要的內存數據複製。這徹底能夠給 LineDataObj 類增長几個靜態方法,判斷出數據包的行號和 RGB 類型,而後將數據部分寫入到 LineDataObj 的數據字段中。這樣作不只能夠減小內存讀寫的次數,並且能夠在一個對象中申請大段內存,保存整行的數據,最後寫入到圖片時,只用將這個區域賦值到圖片中便可。
最後分享一下如何在 Qt 中高效的顯示圖片。通常用 Qt 顯示圖片能夠用 QLabel:
QLabel label; QImage image; // 執行一些讀取圖片的操做,再顯示在 QLabel 上 label.setPixmap(QPixmap::fromImage(image));
可是用 QPixmap::fromImage 會從 image 的內存區域中複製一份數據到 Pixmap 中,這樣的操做並不高效。咱們可使用 QImage::scanLine 方法獲取它對應的內存區域,直接對內存進行操做,顯示的時候不用 QPixmap::fromImage,咱們要直接將內存中的修改顯示到界面中,這樣咱們要定義一個類(不妨讓它繼承 QLabel
),重寫 paintEvent 方法:
void PictureImage::paintEvent(QPaintEvent *event) { Q_UNUSED(event); if(m_index == uchar(-1)) { return; } // this->painter.drawImage(target, *m_image); QPainter p(this); // target 在構造函數中定義: // target = QRectF(0.0, 0.0, PictureImage::ImageWidth, PictureImage::ImageHeight); p.drawImage(target, *m_images[m_index]); }
像 p.drawImage(target, image)
這樣就能夠將圖片更新到界面中,而且它會被 QPixmap 的 fromImage 方法要高效。
以前說過,模擬數據是用 Python 代碼編寫的,這個代碼發送模擬數據的效率能夠高達 100M/s,下面的截圖是我在本身的筆記本(i5 8200U@1.8G)上運行的結果:
可是令我感到特別奇怪的是,有一段時間一樣的代碼在個人 amd ryzen 1500x@3.5G 臺式機上只能達到 50M/s 的速度。我一度懷疑是英特爾和 AMD 的處理器單核性能有差別,但按道理不該該有這麼大的速度差別。並且最近幾天它又在個人臺式機上可以跑到 100M/s 的速度。