web-dev-qa-db-ja.com

信号とスロットを多用すると、アプリケーションのパフォーマンスに影響しますか?

質問は教育目的で行われます。

2つのオブジェクト(たとえば2つのスレッド)間で30〜50以上の信号と​​スロットのペアを使用すると、アプリケーションのパフォーマンス、実行時間、または応答時間に影響しますか?

32
lucab0ni

まず第一に、おそらくQThreadsにスロットを入れないでください。 QThreadsは、runメソッドとプライベートメソッド(シグナルではありません!)を再実装する以外の方法で派生することを意図したものではありません。

QThreadは、概念的にはスレッドコントローラーであり、スレッド自体ではありません。ほとんどの場合、QObjectを処理する必要があります。スレッドを開始してから、オブジェクトインスタンスをそのスレッドに移動します。これが、スレッドでスロットを正しく機能させる唯一の方法です。スレッドインスタンス(それQObjectから派生しています!)をスレッドに移動することは、ハックであり、悪いスタイルです。情報に基づいていないフォーラムの投稿が別の方法で言っているにもかかわらず、それをしないでください。

残りの質問については、シグナルスロット呼び出しは何も見つけたり検証したりする必要はありません。 「ロケーション」と「検証」は、接続が確立されたときに行われます。通話時に行われる主な手順は次のとおりです。

  1. プールからのシグナルスロットミューテックスのロック。

  2. 接続リストを繰り返し処理します。

  3. 直接呼び出しまたはキューに入れられた呼び出しのいずれかを使用して呼び出しを実行します。

共通コスト

シグナルスロット呼び出しは、mocによって生成されたシグナルの実装では常に直接呼び出しとして開始されます。信号の引数へのポインタの配列がスタック上に構築されます。引数はコピーされません。

次に、信号は QMetaObject::activate を呼び出します。ここで、接続リストのミューテックスが取得され、接続されたスロットのリストが繰り返され、各スロットが呼び出されます。

直接接続

スロットは、接続の確立時に取得したQObject::qt_static_metacallを直接呼び出すか、QObject::qt_metacallを使用して接続をセットアップした場合はQMetaObject::connectを呼び出すことによって呼び出されます。後者では、 信号とスロットの動的作成 が可能です。

キュー接続

呼び出しはイベントキューに格納する必要があり、シグナルを返す必要があるため、引数をマーシャリングしてコピーする必要があります。これは、ポインタの配列をコピーに割り当て、ヒープ上の各引数をコピー構成することによって行われます。それを行うためのコードは、実際には飾り気のない単純な古いCです。

呼び出しのキューイングは queued_activate 内で行われます。ここでコピー構築が行われます。

キューに入れられた呼び出しのオーバーヘッドは、常にQMetaCallEventの少なくとも1つのヒープ割り当てです。呼び出しに引数がある場合は、引数へのポインタ配列が割り当てられ、引数ごとに追加の割り当てが行われます。 n引数を使用した呼び出しの場合、C式として指定されるコストは(n ? 2+n : 1)割り当てです。呼び出しをブロックするための戻り値は、引数としてのカウンターです。間違いなく、Qtのこの側面は、すべてに対して1つの割り当てに最適化できますが、実際には、些細なメソッドを呼び出す場合にのみ問題になります。

ベンチマーク結果

直接の(キューに入れられていない)シグナルスロット呼び出しでさえ、測定可能なオーバーヘッドがありますが、戦闘を選択する必要があります。コードとパフォーマンスの設計のしやすさ。最終的なアプリケーションのパフォーマンスを測定し、ボトルネックを特定しますか?そうした場合、実際のアプリケーションでは、信号スロットのオーバーヘッドは何の役割も果たさないことに気付くでしょう。

シグナルスロットメカニズムに大きなオーバーヘッドがあるのは、些細な関数を呼び出す場合だけです。たとえば、以下のコードでtrivialスロットを呼び出す場合。これは完全なスタンドアロンベンチマークなので、自由に実行して自分の目で確かめてください。私のマシンでの結果は次のとおりです。

Warming up the caches...
trivial direct call took 3ms
nonTrivial direct call took 376ms
trivial direct signal-slot call took 158ms, 5166% longer than direct call.
nonTrivial direct signal-slot call took 548ms, 45% longer than direct call.
trivial queued signal-slot call took 2474ms, 1465% longer than direct signal-slot and 82366% longer than direct call.
nonTrivial queued signal-slot call took 2474ms, 416% longer than direct signal-slot and 653% longer than direct call.

おそらく、文字列の連結は非常に高速であることに注意する必要があります:)

関数ポインタを介して呼び出しを行っていることに注意してください。これは、コンパイラが加算関数への直接呼び出しを最適化するのを防ぐためです。

//main.cpp
#include <cstdio>
#include <QCoreApplication>
#include <QObject>
#include <QTimer>
#include <QElapsedTimer>
#include <QTextStream>

static const int n = 1000000;

class Test : public QObject
{
    Q_OBJECT
public slots:
    void trivial(int*, int, int);
    void nonTrivial(QString*, const QString&, const QString&);
signals:
    void trivialSignalD(int*, int, int);
    void nonTrivialSignalD(QString*, const QString&, const QString &);
    void trivialSignalQ(int*, int, int);
    void nonTrivialSignalQ(QString*, const QString&, const QString &);
private slots:
    void run();
private:
    void benchmark(bool timed);
    void testTrivial(void (Test::*)(int*,int,int));
    void testNonTrivial(void (Test::*)(QString*,const QString&, const QString&));
public:
    Test();
};

Test::Test()
{
    connect(this, SIGNAL(trivialSignalD(int*,int,int)),
            SLOT(trivial(int*,int,int)), Qt::DirectConnection);
    connect(this, SIGNAL(nonTrivialSignalD(QString*,QString,QString)),
            SLOT(nonTrivial(QString*,QString,QString)), Qt::DirectConnection);
    connect(this, SIGNAL(trivialSignalQ(int*,int,int)),
            SLOT(trivial(int*,int,int)), Qt::QueuedConnection);
    connect(this, SIGNAL(nonTrivialSignalQ(QString*,QString,QString)),
            SLOT(nonTrivial(QString*,QString,QString)), Qt::QueuedConnection);
    QTimer::singleShot(100, this, SLOT(run()));
}

void Test::run()
{
    // warm up the caches
    benchmark(false);
    // do the benchmark
    benchmark(true);
}

void Test::trivial(int * c, int a, int b)
{
    *c = a + b;
}

void Test::nonTrivial(QString * c, const QString & a, const QString & b)
{
    *c = a + b;
}

void Test::testTrivial(void (Test::* method)(int*,int,int))
{
    static int c;
    int a = 1, b = 2;
    for (int i = 0; i < n; ++i) {
        (this->*method)(&c, a, b);
    }
}

void Test::testNonTrivial(void (Test::* method)(QString*, const QString&, const QString&))
{
    static QString c;
    QString a(500, 'a');
    QString b(500, 'b');
    for (int i = 0; i < n; ++i) {
        (this->*method)(&c, a, b);
    }
}

static int pct(int a, int b)
{
    return (100.0*a/b) - 100.0;
}

void Test::benchmark(bool timed)
{
    const QEventLoop::ProcessEventsFlags evFlags =
            QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers;
    QTextStream out(stdout);
    QElapsedTimer timer;
    quint64 t, nt, td, ntd, ts, nts;

    if (!timed) out << "Warming up the caches..." << endl;

    timer.start();
    testTrivial(&Test::trivial);
    t = timer.elapsed();
    if (timed) out << "trivial direct call took " << t << "ms" << endl;

    timer.start();
    testNonTrivial(&Test::nonTrivial);
    nt = timer.elapsed();
    if (timed) out << "nonTrivial direct call took " << nt << "ms" << endl;

    QCoreApplication::processEvents(evFlags);

    timer.start();
    testTrivial(&Test::trivialSignalD);
    QCoreApplication::processEvents(evFlags);
    td = timer.elapsed();
    if (timed) {
        out << "trivial direct signal-slot call took " << td << "ms, "
               << pct(td, t) << "% longer than direct call." << endl;
    }

    timer.start();
    testNonTrivial(&Test::nonTrivialSignalD);
    QCoreApplication::processEvents(evFlags);
    ntd = timer.elapsed();
    if (timed) {
        out << "nonTrivial direct signal-slot call took " << ntd << "ms, "
               << pct(ntd, nt) << "% longer than direct call." << endl;
    }

    timer.start();
    testTrivial(&Test::trivialSignalQ);
    QCoreApplication::processEvents(evFlags);
    ts = timer.elapsed();
    if (timed) {
        out << "trivial queued signal-slot call took " << ts << "ms, "
               << pct(ts, td) << "% longer than direct signal-slot and "
               << pct(ts, t) << "% longer than direct call." << endl;
    }

    timer.start();
    testNonTrivial(&Test::nonTrivialSignalQ);
    QCoreApplication::processEvents(evFlags);
    nts = timer.elapsed();
    if (timed) {
        out << "nonTrivial queued signal-slot call took " << nts << "ms, "
               << pct(nts, ntd) << "% longer than direct signal-slot and "
               << pct(nts, nt) << "% longer than direct call." << endl;
    }
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Test t;
    return a.exec();
}

#include "main.moc"
61
Kuba Ober

もちろん、これらはアプリケーションのパフォーマンスに影響を与えます。これは主に、接続オブジェクトの検索+スロットオブジェクトの状態nの検証に費やされる時間のためです。しかし、信号とスロットのメカニズムの単純さと柔軟性は、オーバーヘッドに見合うだけの価値があります。Plus one of the major advantage of signal-slot mechanism is they are type=safe allowing communication between objects, irrespective of type of object unlike callbacks.

コールバックと比較すると、シグナルとスロットは柔軟性が向上しているため、わずかに遅くなりますが、実際のアプリケーションとの違いはわずかです。一般に、一部のスロットに接続された信号を発信することは、非仮想関数呼び出しを使用してレシーバーを直接呼び出すよりも約10倍遅くなります。これは、接続オブジェクトの場所を特定し、すべての接続を安全に反復し(つまり、後続のレシーバーがエミッション中に破壊されていないことを確認する)、一般的な方法でパラメーターをマーシャリングするために必要なオーバーヘッドです。 10回の非仮想関数呼び出しは多くのように聞こえるかもしれませんが、たとえば、新規操作や削除操作よりもはるかに少ないオーバーヘッドです。舞台裏で新規または削除が必要な文字列、ベクトル、またはリスト操作を実行するとすぐに、シグナルとスロットのオーバーヘッドは、完全な関数呼び出しコストのごく一部にしか関与しません。

ソース: 信号とスロット

5
ScarCode