使用 Qt 獲取 UDP 數據並顯示成圖片(2)

本文首發於 BriFuture 的 我的博客html

在個人前一篇文章 使用 Qt 獲取 UDP 數據並顯示成圖片 中,我講了如何用 Python 模擬發送數據,如何在 Qt 中高效的接收 UDP 數據包並將數據解析出來。然而此前的文章在分別顯示 RGB 通道、R 通道、G 通道、B 通道這四組通道的圖片時仍然會出現處理速度過慢的問題。python

前面說過編寫的程序至少會用到 3 個線程來分別處理 UI、socket 數據、數據解析,由於不這樣作無法在時限內處理完接收到的數據,寫第一篇博客的時候,我覺得是單純的使用 new 在堆中分配內存致使程序運行效率低,後來確實經過預分配對象內存解決了部分問題,可是還有一些會影響程序運行速度的問題沒有解決,也沒有深究,今天從新編寫代碼的時候,爲了分配數據到 4 幅圖片上(分別是 RGB 通道、R 通道、G 通道、B 通道),發現運行速度仍是不夠,影響運行速度的緣由有幾個:數組

  1. 運行程序的模式(Debug 和 Release 兩種模式)
  2. Qt 的事件循環機制(以前反覆懷疑過,不過最後仍是發現短期內大量調用信號很容易致使處理速度過慢)
  3. 低效的內存複製操做(如 QByteArray 的 assign 賦值操做和過多、過於複雜的程序流程)

接下來看看這幾個致使程序運行速度不夠的緣由:緩存

1. Qt Debug 模式和 Release 模式的差別

在 QtCreator 中運行程序,若是是以 Debug 模式運行的話,速度是要比 Release 模式低一些的。之前編寫 Qt 程序,數據量通常不大,對於性能都沒有要求,即便程序代碼不夠優化,但在用戶使用過程當中通常不會感覺到運行卡頓,因此一直都沒發現 Debug 模式和 Release 模式的性能有差別。多線程

不過其實也能猜到性能有差別的大概緣由:Debug 模式下會在最終生成的代碼裏面插入不少額外的代碼用於調試,可是 Release 生成的代碼是不會插入這些調試用的代碼的,最明顯的差別就是 Debug 模式生成的可執行文件比 Release 模式生成的可執行文件要大得多。app

Debug 模式下運行程序,實際 FPS 和指望的 FPS 有 6 幀的差距,差距產生的緣由是處理速度不夠,致使最終生成圖片的速度慢了。socket

Release 模式下即便是原始數據包的指望 FPS 到了 77 幀,實際的 FPS 也能夠達到 77 幀,也就是說在處理過程當中沒有出現處理速度跟不上接受數據的速度。函數

2. Qt 的事件循環機制

當咱們使用 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 的一個子線程中運行循環代碼,忽略掉該子線程中的事件循環以提升程序的性能。

3. 低效的內存複製操做

在接收到原始字節數據以後,最重要也是最麻煩的就是解析數據。包括識別自定義協議數據的頭部信息,將數據包中的圖像數據複製到緩衝區,並將緩衝區中的數據以圖片的形式顯示出來。接下來分享幾個高效處理數據的幾個小技巧:

  1. 使用 QByteArray 存儲原始數據包時,先調用 resize 預分配內存,而後使用 memcpy 直接對內存數據進行操做,這樣作效率是最高的,但它也是比較繁瑣的。
QByteArray data;
data.resize(PacketSize); // PacketSize 是預先定義好的數據字節數
// 能夠簡單的認爲 rawData 就是從 UDP 端口中接收到的數據,
// ValidDataSize 也是預先定義好的數據字段的長度
memcpy(rawData.data(), data.data(), ValidDataSize);

// 上面的代碼要比直接使用賦值操做 = 高效
data = rawData;
  1. 若是接收到的數據能夠明確是有序的,能夠用數據分別表示相應的序號,再從數組中取數據,我最開始存儲 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(...);
  1. 儘可能保持清晰並且簡單的結構。我以前寫代碼總想着考慮到全部狀況,最終卻老是無法盡善盡美只有根據狀況放低預期,我以爲沒必要一開始就非要把代碼的層次結構劃分的特別詳細,根據實際狀況使用合理的程序結構(固然每一個人可能有不一樣的見解,但少便是多的原則確實給了我很大的啓發)。

我以前編寫程序時,除了有一個 LineDataObj 用來表示行對象,還有一個 RawDataObj 表示原始的數據包對象。處理的流程多:1. 接受原始數據包 => 2. 將數據包填充到 RawDataObj 中並解析數據包的行號,RGB 類型 => 3. 根據 RawDataObj 的屬性肯定對應的 LineDataObj => 4. 當 LineDataObj 存儲到必定數目時生成圖像。

這個流程很直觀也很容易想到,可是 RawDataObj 這個數據對象其實不必使用,由於它增長了一次沒必要要的內存數據複製。這徹底能夠給 LineDataObj 類增長几個靜態方法,判斷出數據包的行號和 RGB 類型,而後將數據部分寫入到 LineDataObj 的數據字段中。這樣作不只能夠減小內存讀寫的次數,並且能夠在一個對象中申請大段內存,保存整行的數據,最後寫入到圖片時,只用將這個區域賦值到圖片中便可。

4. 高效地顯示圖片

最後分享一下如何在 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 發送模擬數據遇到的問題

以前說過,模擬數據是用 Python 代碼編寫的,這個代碼發送模擬數據的效率能夠高達 100M/s,下面的截圖是我在本身的筆記本(i5 8200U@1.8G)上運行的結果:

可是令我感到特別奇怪的是,有一段時間一樣的代碼在個人 amd ryzen 1500x@3.5G 臺式機上只能達到 50M/s 的速度。我一度懷疑是英特爾和 AMD 的處理器單核性能有差別,但按道理不該該有這麼大的速度差別。並且最近幾天它又在個人臺式機上可以跑到 100M/s 的速度。

參考

線程、事件與QObject

相關文章
相關標籤/搜索