Ограничения QUdpSocket в высокочастотных средах

У меня есть задача обработки данных UDP со скоростью чтения ~ 10 кГц. Я работаю с Qt 5.13.1 (MinGW32), поэтому попытался использовать QUdpSocket.
Я сделал простую тестовую программу, но результаты немного разочаровывают. Сигнал readyRead() слишком медленный. По какой-то причине я получаю задержки более 1 или 2 мс каждые 2-4 сигнала.
Я сделал простой счетчик пакетов и сравнил его с тем, что я вижу в wireshark. Конечно, есть потеря пакетов.

Что я могу сделать, чтобы повысить производительность? Или, может быть, это просто предел цикла событий Qt?

Я запускаю его с помощью Qt Creator 4.10.0. В Windows 7.

Обновление: С вашими советами: я пытался:

  1. Перемещение сокета в другой поток. Это дает немного больше производительности .. очень немного

  2. LowDelayOption=1 - изменений не заметил

  3. ReceiveBufferSizeSocketOption — изменений не заметил

  4. Нет использования QDebug при чтении - не проверял, использую только для сбора статистики

udpproc.h

#ifndef UDPPROC_H
#define UDPPROC_H

#include "QObject"
#include "QUdpSocket"
#include "QHostAddress"
#include "QThread"

#include "QDebug"
#include "networker.h"

class UDPProc : public QObject
{
    Q_OBJECT
public:
    UDPProc();
    ~UDPProc();
private:
    QUdpSocket dataServerSocket;
    NetWorker* netWorker;
    QThread netThread;


};

#endif // UDPPROC_H

udpproc.cpp

УДППрок::УДППрок() {

netWorker = new NetWorker(&dataServerSocket);
netWorker->moveToThread(&netThread);
netWorker->getInnerLoop()->moveToThread(&netThread);

connect(&netThread, SIGNAL(started()), netWorker, SLOT(serverSocketProccessing()));
connect(&this->dataServerSocket, SIGNAL(readyRead()), netWorker->getInnerLoop(), SLOT(quit()));

QString address = "127.0.0.3:16402";
QHostAddress ip(address.split(":").at(0));
quint16 port = address.split(":").at(1).toUShort();
dataServerSocket.bind(ip, port);

//dataServerSocket.setSocketOption(QAbstractSocket::LowDelayOption, 1);
dataServerSocket.moveToThread(&netThread);

netThread.start();

//dataServerSocket.setSocketOption(QAbstractSocket::ReceiveBufferSizeSocketOption, 128000);
//qDebug()<<dataServerSocket.socketOption(QAbstractSocket::ReceiveBufferSizeSocketOption).toInt();

}

networker.h

#ifndef NETWORKER_H
#define NETWORKER_H

#include <QObject>
#include "QElapsedTimer"
#include "QEventLoop"
#include "QUdpSocket"
#include "QVector"

class NetWorker : public QObject
{
    Q_OBJECT
private:

    QElapsedTimer timer;
    QVector<long long> times;

    QEventLoop loop;
    QUdpSocket *dataServerSocket;

    char buffer[16286];
    int cnt = 0;
public:
    NetWorker(QUdpSocket *dataServerSocket);
    ~NetWorker();
    QEventLoop * getInnerLoop();

public slots:
    void serverSocketProccessing();

};

#endif // NETWORKER_H

networker.cpp

#include "networker.h"

NetWorker::NetWorker(QUdpSocket *dataServerSocket)
{
    this->dataServerSocket = dataServerSocket;
}

NetWorker::~NetWorker()
{
    delete dataServerSocket;
}

QEventLoop *NetWorker::getInnerLoop()
{
    return &loop;
}

void NetWorker::serverSocketProccessing()
{
    while(true){
        timer.start();
        loop.exec();
        times<<timer.nsecsElapsed();
        while(dataServerSocket->hasPendingDatagrams()){
            dataServerSocket->readDatagram(buffer, dataServerSocket->pendingDatagramSize());
        }
        if (times.size() >= 10000){
            long long sum = 0;
            for (int x : times){
                //qDebug()<<x;
                sum += x;
            }
            qDebug() << "mean: "<<sum/times.size();
            break;
        }

    }
}

person Letargon    schedule 28.10.2019    source источник
comment
Первое, что я бы сделал, это переместил чтение UDP в его собственный отдельный/выделенный QThread (если вы еще этого не сделали); если он работает в основном потоке Qt/GUI, то ему придется время от времени ждать завершения несвязанных операций с графическим интерфейсом, прежде чем он сможет выполниться. Также может помочь вызов setSocketOption(ReceiveBufferSizeSocketOption, someLargeNumber);, чтобы увеличить буфер приема сокета, и/или установить потоку приема UDP более высокий приоритет выполнения.   -  person Jeremy Friesner    schedule 28.10.2019
comment
@Jeremy Я запускаю его в консольном приложении без графического интерфейса, поэтому я не уверен, есть ли несвязанные сигналы. И я попытался установить разные значения для ReceiveBufferSizeSocketOption (проверив себя с помощью socketOption (ReceiveBufferSizeSocketOption), но безрезультатно.   -  person Letargon    schedule 28.10.2019
comment
В таком случае вам лучше выделить отдельный поток для блокирующего чтения.   -  person Botje    schedule 28.10.2019
comment
Если запустить его в отдельном потоке, то можно быть уверенным, что несвязанных сигналов нет.   -  person Jeremy Friesner    schedule 28.10.2019
comment
@ Джереми, о, это слишком сложно оценивать, каждый раз появляются новые цифры. Просто запустите его в отдельном потоке, используя цикл событий, чтобы дождаться сигнала. Я оценил его средним временем ожидания для 10000 пакетов и получил 6 мс. То же самое (+-1 мс) для старой версии. На этот раз без qdebug при чтении. Наверное, я просто хочу слишком многого. Я был уверен, что 1 мс — это очень долго для оптимизации.   -  person Letargon    schedule 29.10.2019


Ответы (2)


Вы никогда не сможете получать сокет-пакеты в Windows с такой высокой скоростью. Это предел операционной системы. Даже при использовании QAbstractSocket::LowDelayOption и при перемещении вашего принимающего кода в бесконечный цикл, например:

socket->setSocketOption(QAbstractSocket::LowDelayOption, 1);
...

for (;;)
{
    if(socket->waitForReadyRead(1)) // waits for some events anyway
    {
        // read here
    }
}

В качестве альтернативы вы можете встроить некоторое поле временного кода в структуру пакета данных и вместо этого отправить несколько пакетов вместе или использовать какое-либо соединение, при котором пакеты не теряются. Например, используйте TCP-соединение + транзакции, поскольку следующие ситуации возможны для сокета:

  • Полный пакет получен
  • Получил только часть пакета
  • Получил несколько пакетов вместе

Кроме того, не пытайтесь изменить readBufferSize:

Если размер буфера ограничен определенным размером, QAbstractSocket не будет буферизовать больше данных, чем этот размер. В исключительных случаях размер буфера, равный 0, означает, что буфер чтения неограничен и все входящие данные буферизуются. Это значение по умолчанию.

Эта опция полезна, если вы читаете данные только в определенные моменты времени (например, в потоковом приложении в реальном времени) или если вы хотите защитить свой сокет от получения слишком большого количества данных, что может в конечном итоге привести к тому, что ваше приложение исчерпает ресурсы. Память.

Только QTcpSocket использует внутренний буфер QAbstractSocket; QUdpSocket вообще не использует никакой буферизации, а полагается на неявную буферизацию, предоставляемую операционной системой. Из-за этого вызов этой функции для QUdpSocket не имеет никакого эффекта.

person Vladimir Bershov    schedule 29.10.2019
comment
То есть скорость 1мс-15мс для ловли одного сигнала это нормально? Я могу принять некоторую потерю пакетов, но ожидание сигнала сейчас занимает больше всего времени. Изначально я просто хотел записать данные этих пакетов в файл, но мне требуется больше времени, чтобы поймать сигнал, чем на самом деле обработать данные. - person Letargon; 29.10.2019
comment
@Letargon Да, задержки подходят для всего, что зависит от системы событий в Windows. Вы не можете избежать задержки, даже используя цикл. - person Vladimir Bershov; 29.10.2019

При измерении критичных ко времени разделов вашего кода я рекомендую избегать использования qDebug (или любых других медленных функций печати/отладки). Это может иметь слишком большое влияние на ваши фактические измерения.

Я предлагаю вам хранить значения времени, полученные от QElapsedTimer, в отдельном контейнере (например, QVector или просто один qint64, который вы усредняете по времени) и отображать отладочные сообщения только время от времени (каждую секунду или только в конец). Таким образом, накладные расходы, вызванные измерением, оказывают меньшее влияние. Усреднение за более длительный период времени также поможет с дисперсией результатов измерений.

Я также рекомендую вам использовать QElapsedTimer::nsecsElapsed, чтобы избежать проблем с округлением в высокочастотных ситуациях, потому что QElapsedTiemr::elapsed всегда будет округлять до ближайшей миллисекунды (а вы уже измеряете в пределах 1 мс).

Вы всегда можете преобразовать наносекунды в миллисекунды позже, когда фактически показываете результаты.

Каков размер данных, которые вы получаете на частоте 10 кГц?

person parti82    schedule 28.10.2019
comment
В настоящее время я не уверен в требованиях к производительности. Я использую локальную программу в качестве источника данных, и я думаю, что она просто генерирует пакеты с максимальной доступной частотой. Скорость передачи данных составляет около 2,5 МБ. Я просто хочу построить некую эффективную систему. Обновлен код с QVectors. - person Letargon; 29.10.2019