QT signal performance significantly degrades when the number of the connections grow.

In my previous post I compared the performance of a single QT connection with the performance of a notification mechanism based on virtual functions.

In this post I’ll demonstrate how the performance of QT signals degrade when the number of the connections grow. Briefly saying a QT signal becomes 57 times slower than a virtual function.

Below I described my experimentations and provided the full source code.

I added 20 signals that are never emitted to my ValueObject:

#include "Awl/Observable.h"

#include <QObject>

namespace test
{
    class NotifyValueChanged
    {
    public:

        virtual void onValueChanged() = 0;
    };
    
    class ValueObject : 
        public QObject,
        public awl::Observable<NotifyValueChanged>
    {
        Q_OBJECT

    public:

        Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)

        void notifyValueChanged()
        {
            Notify(&NotifyValueChanged::onValueChanged);
        }

    signals:

        void nothingChanged1();
        void nothingChanged2();
        void nothingChanged3();
        void nothingChanged4();
        void nothingChanged5();
        void nothingChanged6();
        void nothingChanged7();
        void nothingChanged8();
        void nothingChanged9();
        void nothingChanged10();
        void nothingChanged11();
        void nothingChanged12();
        void nothingChanged13();
        void nothingChanged14();
        void nothingChanged15();
        void nothingChanged16();
        void nothingChanged17();
        void nothingChanged18();
        void nothingChanged19();
        void nothingChanged20();

        void valueChanged();

    private:

        int value() const
        {
            return m_val;
        }

        void setValue(int val)
        {
            if (m_val != val)
            {
                m_val = val;

                emit valueChanged();
            }
        }

        int m_val = 0;
    };
}

And added onNothingChanged handler to my ObserverObject:

#include "Qtil/Tests/ValueObject.h"

namespace test
{
    class ObserverObject : 
        public QObject,
        public awl::Observer<NotifyValueChanged>
    {
        Q_OBJECT

    public:

        void onValueChanged() override
        {
            ++m_count;
        }

        void onValueChangedSignal()
        {
            ++m_count;
        }

        void onValueChangedSignalSender()
        {
            sender();

            ++m_count;
        }

        void onNothingChanged()
        {
            ++m_nothingCount;
        }

        std::size_t count() const
        {
            return m_count;
        }

    private:

        std::size_t m_count = 0;
        std::size_t m_nothingCount = 0;
    };
}

And in addition to creating my test conntection (at line 75) I connected all this 20 signals the number of times specified by fake_connect_count attribute:

#include "ValueObject.h"
#include "ObserverObject.h"

#include "Qtil/Testing/UnitTest.h"

#include "Qtil/Format.h"

#include "Awl/StopWatch.h"

#include <vector>

using namespace test;

QTIL_UNIT_TEST(SignalTest)
{
    QTIL_ATTRIBUTE(size_t, observer_count, 1000);
    QTIL_ATTRIBUTE(size_t, emit_count, 100000);
    QTIL_ATTRIBUTE(size_t, fake_connect_count, 100);
    QTIL_FLAG(call_sender);
    QTIL_FLAG(separate_handlers);

    {
        ValueObject object;

        std::vector<ObserverObject> observers(observer_count);

        void (ObserverObject::* signal_handler)();

        if (call_sender)
        {
            signal_handler = &ObserverObject::onValueChangedSignalSender;
        }
        else
        {
            signal_handler = &ObserverObject::onValueChangedSignal;
        }
        
        void (ObserverObject:: *fake_handler)();

        if (separate_handlers)
        {
            fake_handler = signal_handler;
        }
        else
        {
            fake_handler = &ObserverObject::onNothingChanged;
        }

        for (ObserverObject& observer : observers)
        {
            for (size_t i = 0; i < fake_connect_count; ++i)
            {
                QObject::connect(&object, &ValueObject::nothingChanged1, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged2, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged3, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged4, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged5, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged6, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged7, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged8, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged9, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged10, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged11, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged12, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged13, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged14, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged15, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged16, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged17, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged18, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged19, &observer, fake_handler, Qt::DirectConnection);
                QObject::connect(&object, &ValueObject::nothingChanged20, &observer, fake_handler, Qt::DirectConnection);
            }

            QObject::connect(&object, &ValueObject::valueChanged, &observer, signal_handler, Qt::DirectConnection);
        }

        {
            awl::StopWatch sw;

            for (size_t i = 0; i < emit_count; ++i)
            {
                emit object.valueChanged();
            }

            context.logger.debug(qtil::Format() << "QT signal: " << sw);
        }

        for (ObserverObject& observer : observers)
        {
            AWT_ASSERT(observer.count() == emit_count);
        }
    }

    {
        ValueObject object;

        std::vector<ObserverObject> observers(observer_count);

        for (ObserverObject& observer : observers)
        {
            object.Subscribe(&observer);
        }

        {
            awl::StopWatch sw;

            for (size_t i = 0; i < emit_count; ++i)
            {
                emit object.notifyValueChanged();
            }

            context.logger.debug(qtil::Format() << "Virtual function: " << sw);
        }
        for (ObserverObject& observer : observers)
        {
            AWT_ASSERT(observer.count() == emit_count);
        }
    }
}

As you can see from the test source code along with fake_connect_count I also added the following test attributes:

  • call_sender – if the signal handler calls sender() function.
  • separate_handlers – connect fake signals to a separate slot.

If I run my new test with zero fake_connect_count nothing changes and I get the same result as with my previous test, but with fake_connect_count set to 5 (and so 100 fake connections) the performance of QT signal significantly degrades and I get the following results:

{
    "call_sender": false,
    "emit_count": 100000,
    "fake_connect_count": 5,
    "separate_handlers": true
}
QT signal: 00:00:09.640
Virtual function: 00:00:00.191

{
    "call_sender": true,
    "emit_count": 100000,
    "fake_connect_count": 5,
    "separate_handlers": true
}
QT signal: 00:00:10.596
Virtual function: 00:00:00.186

{
    "call_sender": false,
    "emit_count": 100000,
    "fake_connect_count": 5,
    "separate_handlers": false
}
QT signal: 00:00:09.612
Virtual function: 00:00:00.198

{
    "call_sender": true,
    "emit_count": 100000,
    "fake_connect_count": 5,
    "separate_handlers": false
}
QT signal: 00:00:10.864
Virtual function: 00:00:00.196

Now QT signal is 57 (10.596 / 0.186) times slower than the virtual function and so we can call 9,437,523 signals per second vs 537,634,408 virtual functions per second.

Let’s take a look at the performance profiler. As we already know sender() takes about 12% and doActivate takes 81% (vs 48% in my previous test):

Consider the following scenario: Binance crypto exchange has 406 BTC markets (like ETH/BTC, XRP/BTC, etc…) and they all are connected to a signal that is emitted when BTC price changes to recalculate their balances in USDT. Assume at peak load we get 10,000 trades per second on BTC/USDT market so the signal is emitted 4,060,000 times per second. Obviously the performance of the QT signal is critical in this scenario.

By the way, degradation speed decreases after 100 connections, with 1000 connections we get 00:00:11s.173ms and with 10000 connections we get 00:00:13ms.252ms.

My environment: a machine with Intel i7 CPU and Windows 64bit, MSVC2022 Compiler Version 19.32.31332.

Leave a Reply

Your email address will not be published. Required fields are marked *