C ++ Tuple vs Struct

91

Gibt es einen Unterschied zwischen der Verwendung von a std::tupleund nur Daten struct?

typedef std::tuple<int, double, bool> foo_t;

struct bar_t {
    int id;
    double value;
    bool dirty;
}

Nach dem, was ich online gefunden habe, habe ich festgestellt, dass es zwei Hauptunterschiede gibt: Der structist besser lesbar, während der tupleviele allgemeine Funktionen hat, die verwendet werden können. Sollte es einen signifikanten Leistungsunterschied geben? Ist das Datenlayout auch miteinander kompatibel (austauschbar gegossen)?

Alex Koay
quelle
Ich habe nur bemerkt, dass ich die Besetzungsfrage vergessen habe : Die Implementierung der tupleis-Implementierung ist definiert, daher hängt sie von Ihrer Implementierung ab. Persönlich würde ich nicht damit rechnen.
Matthieu M.

Antworten:

29

Wir haben eine ähnliche Diskussion über Tupel und Struktur und ich schreibe einige einfache Benchmarks mit Hilfe eines meiner Kollegen, um die Unterschiede in der Leistungsdauer zwischen Tupel und Struktur zu identifizieren. Wir beginnen zunächst mit einer Standardstruktur und einem Tupel.

struct StructData {
    int X;
    int Y;
    double Cost;
    std::string Label;

    bool operator==(const StructData &rhs) {
        return std::tie(X,Y,Cost, Label) == std::tie(rhs.X, rhs.Y, rhs.Cost, rhs.Label);
    }

    bool operator<(const StructData &rhs) {
        return X < rhs.X || (X == rhs.X && (Y < rhs.Y || (Y == rhs.Y && (Cost < rhs.Cost || (Cost == rhs.Cost && Label < rhs.Label)))));
    }
};

using TupleData = std::tuple<int, int, double, std::string>;

Wir verwenden dann Celero, um die Leistung unserer einfachen Struktur und unseres Tupels zu vergleichen. Nachfolgend finden Sie den Benchmark-Code und die Leistungsergebnisse, die mit gcc-4.9.2 und clang-4.0.0 erfasst wurden:

std::vector<StructData> test_struct_data(const size_t N) {
    std::vector<StructData> data(N);
    std::transform(data.begin(), data.end(), data.begin(), [N](auto item) {
        std::random_device rd;
        std::mt19937 gen(rd());
        std::uniform_int_distribution<> dis(0, N);
        item.X = dis(gen);
        item.Y = dis(gen);
        item.Cost = item.X * item.Y;
        item.Label = std::to_string(item.Cost);
        return item;
    });
    return data;
}

std::vector<TupleData> test_tuple_data(const std::vector<StructData> &input) {
    std::vector<TupleData> data(input.size());
    std::transform(input.cbegin(), input.cend(), data.begin(),
                   [](auto item) { return std::tie(item.X, item.Y, item.Cost, item.Label); });
    return data;
}

constexpr int NumberOfSamples = 10;
constexpr int NumberOfIterations = 5;
constexpr size_t N = 1000000;
auto const sdata = test_struct_data(N);
auto const tdata = test_tuple_data(sdata);

CELERO_MAIN

BASELINE(Sort, struct, NumberOfSamples, NumberOfIterations) {
    std::vector<StructData> data(sdata.begin(), sdata.end());
    std::sort(data.begin(), data.end());
    // print(data);

}

BENCHMARK(Sort, tuple, NumberOfSamples, NumberOfIterations) {
    std::vector<TupleData> data(tdata.begin(), tdata.end());
    std::sort(data.begin(), data.end());
    // print(data);
}

Mit clang-4.0.0 gesammelte Leistungsergebnisse

Celero
Timer resolution: 0.001000 us
-----------------------------------------------------------------------------------------------------------------------------------------------
     Group      |   Experiment    |   Prob. Space   |     Samples     |   Iterations    |    Baseline     |  us/Iteration   | Iterations/sec  | 
-----------------------------------------------------------------------------------------------------------------------------------------------
Sort            | struct          | Null            |              10 |               5 |         1.00000 |    196663.40000 |            5.08 | 
Sort            | tuple           | Null            |              10 |               5 |         0.92471 |    181857.20000 |            5.50 | 
Complete.

Und Leistungsergebnisse, die mit gcc-4.9.2 gesammelt wurden

Celero
Timer resolution: 0.001000 us
-----------------------------------------------------------------------------------------------------------------------------------------------
     Group      |   Experiment    |   Prob. Space   |     Samples     |   Iterations    |    Baseline     |  us/Iteration   | Iterations/sec  | 
-----------------------------------------------------------------------------------------------------------------------------------------------
Sort            | struct          | Null            |              10 |               5 |         1.00000 |    219096.00000 |            4.56 | 
Sort            | tuple           | Null            |              10 |               5 |         0.91463 |    200391.80000 |            4.99 | 
Complete.

Aus den obigen Ergebnissen können wir das klar erkennen

  • Tupel ist schneller als eine Standardstruktur

  • Binäre Produkte von clang haben eine höhere Leistung als gcc. clang-vs-gcc ist nicht der Zweck dieser Diskussion, daher werde ich nicht ins Detail gehen.

Wir alle wissen, dass das Schreiben eines == oder <oder> Operators für jede einzelne Strukturdefinition eine schmerzhafte und fehlerhafte Aufgabe sein wird. Ersetzen Sie unseren benutzerdefinierten Komparator durch std :: tie und führen Sie unseren Benchmark erneut aus.

bool operator<(const StructData &rhs) {
    return std::tie(X,Y,Cost, Label) < std::tie(rhs.X, rhs.Y, rhs.Cost, rhs.Label);
}

Celero
Timer resolution: 0.001000 us
-----------------------------------------------------------------------------------------------------------------------------------------------
     Group      |   Experiment    |   Prob. Space   |     Samples     |   Iterations    |    Baseline     |  us/Iteration   | Iterations/sec  | 
-----------------------------------------------------------------------------------------------------------------------------------------------
Sort            | struct          | Null            |              10 |               5 |         1.00000 |    200508.20000 |            4.99 | 
Sort            | tuple           | Null            |              10 |               5 |         0.90033 |    180523.80000 |            5.54 | 
Complete.

Jetzt können wir sehen, dass die Verwendung von std :: tie unseren Code eleganter macht und es schwieriger ist, Fehler zu machen. Wir verlieren jedoch etwa 1% Leistung. Ich werde vorerst bei der std :: tie-Lösung bleiben, da ich auch eine Warnung zum Vergleichen von Gleitkommazahlen mit dem benutzerdefinierten Komparator erhalte.

Bis jetzt haben wir noch keine Lösung, um unseren Strukturcode schneller laufen zu lassen. Schauen wir uns die Swap-Funktion an und schreiben Sie sie neu, um zu sehen, ob wir eine Leistung erzielen können:

struct StructData {
    int X;
    int Y;
    double Cost;
    std::string Label;

    bool operator==(const StructData &rhs) {
        return std::tie(X,Y,Cost, Label) == std::tie(rhs.X, rhs.Y, rhs.Cost, rhs.Label);
    }

    void swap(StructData & other)
    {
        std::swap(X, other.X);
        std::swap(Y, other.Y);
        std::swap(Cost, other.Cost);
        std::swap(Label, other.Label);
    }  

    bool operator<(const StructData &rhs) {
        return std::tie(X,Y,Cost, Label) < std::tie(rhs.X, rhs.Y, rhs.Cost, rhs.Label);
    }
};

Mit clang-4.0.0 gesammelte Leistungsergebnisse

Celero
Timer resolution: 0.001000 us
-----------------------------------------------------------------------------------------------------------------------------------------------
     Group      |   Experiment    |   Prob. Space   |     Samples     |   Iterations    |    Baseline     |  us/Iteration   | Iterations/sec  | 
-----------------------------------------------------------------------------------------------------------------------------------------------
Sort            | struct          | Null            |              10 |               5 |         1.00000 |    176308.80000 |            5.67 | 
Sort            | tuple           | Null            |              10 |               5 |         1.02699 |    181067.60000 |            5.52 | 
Complete.

Und die mit gcc-4.9.2 gesammelten Leistungsergebnisse

Celero
Timer resolution: 0.001000 us
-----------------------------------------------------------------------------------------------------------------------------------------------
     Group      |   Experiment    |   Prob. Space   |     Samples     |   Iterations    |    Baseline     |  us/Iteration   | Iterations/sec  | 
-----------------------------------------------------------------------------------------------------------------------------------------------
Sort            | struct          | Null            |              10 |               5 |         1.00000 |    198844.80000 |            5.03 | 
Sort            | tuple           | Null            |              10 |               5 |         1.00601 |    200039.80000 |            5.00 | 
Complete.

Jetzt ist unsere Struktur etwas schneller als die eines Tupels (ungefähr 3% mit clang und weniger als 1% mit gcc). Wir müssen jedoch unsere angepasste Swap-Funktion für alle unsere Strukturen schreiben.

hungptit
quelle
24

Wenn Sie mehrere verschiedene Tupel in Ihrem Code verwenden, können Sie die Anzahl der von Ihnen verwendeten Funktoren reduzieren. Ich sage das, weil ich oft die folgenden Arten von Funktoren verwendet habe:

template<int N>
struct tuple_less{
    template<typename Tuple>
    bool operator()(const Tuple& aLeft, const Tuple& aRight) const{
        typedef typename boost::tuples::element<N, Tuple>::type value_type;
        BOOST_CONCEPT_REQUIRES((boost::LessThanComparable<value_type>));

        return boost::tuples::get<N>(aLeft) < boost::tuples::get<N>(aRight);
    }
};

Dies mag wie ein Overkill erscheinen, aber für jede Stelle innerhalb der Struktur müsste ich ein ganz neues Funktorobjekt mit einer Struktur erstellen, aber für ein Tupel ändere ich einfach N . Besser als das, ich kann dies für jedes einzelne Tupel tun, anstatt für jede Struktur und für jede Mitgliedsvariable einen ganz neuen Funktor zu erstellen. Wenn ich N Strukturen mit M Mitgliedsvariablen habe, die NxM-Funktoren sind, müsste ich sie erstellen (Worst-Case-Szenario), die auf ein kleines Stück Code komprimiert werden können.

Wenn Sie sich für den Tupel-Weg entscheiden, müssen Sie natürlich auch Enums erstellen, um mit ihnen arbeiten zu können:

typedef boost::tuples::tuple<double,double,double> JackPot;
enum JackPotIndex{
    MAX_POT,
    CURRENT_POT,
    MIN_POT
};

und boom, dein Code ist vollständig lesbar:

double guessWhatThisIs = boost::tuples::get<CURRENT_POT>(someJackPotTuple);

weil es sich selbst beschreibt, wenn Sie die darin enthaltenen Elemente erhalten möchten.

Wheaties
quelle
7
Äh ... C ++ hat Funktionszeiger, template <typename C, typename T, T C::*> struct struct_less { template <typename C> bool operator()(C const&, C const&) const; };sollte also möglich sein. Es ist etwas weniger bequem, es zu buchstabieren, aber es wird nur einmal geschrieben.
Matthieu M.
6

Nun, hier ist ein Benchmark, der keine Struktur von Tupeln innerhalb des struct-Operators == () erstellt. Es stellt sich heraus, dass die Verwendung von Tupel erhebliche Auswirkungen auf die Leistung hat, da zu erwarten ist, dass die Verwendung von PODs überhaupt keine Auswirkungen auf die Leistung hat. (Der Adressauflöser findet den Wert in der Befehlspipeline, bevor die Logikeinheit ihn überhaupt sieht.)

Häufige Ergebnisse, wenn dies auf meinem Computer mit VS2015CE unter Verwendung der Standardeinstellungen für "Release" ausgeführt wird:

Structs took 0.0814905 seconds.
Tuples took 0.282463 seconds.

Bitte Affe damit, bis Sie zufrieden sind.

#include <iostream>
#include <string>
#include <tuple>
#include <vector>
#include <random>
#include <chrono>
#include <algorithm>

class Timer {
public:
  Timer() { reset(); }
  void reset() { start = now(); }

  double getElapsedSeconds() {
    std::chrono::duration<double> seconds = now() - start;
    return seconds.count();
  }

private:
  static std::chrono::time_point<std::chrono::high_resolution_clock> now() {
    return std::chrono::high_resolution_clock::now();
  }

  std::chrono::time_point<std::chrono::high_resolution_clock> start;

};

struct ST {
  int X;
  int Y;
  double Cost;
  std::string Label;

  bool operator==(const ST &rhs) {
    return
      (X == rhs.X) &&
      (Y == rhs.Y) &&
      (Cost == rhs.Cost) &&
      (Label == rhs.Label);
  }

  bool operator<(const ST &rhs) {
    if(X > rhs.X) { return false; }
    if(Y > rhs.Y) { return false; }
    if(Cost > rhs.Cost) { return false; }
    if(Label >= rhs.Label) { return false; }
    return true;
  }
};

using TP = std::tuple<int, int, double, std::string>;

std::pair<std::vector<ST>, std::vector<TP>> generate() {
  std::mt19937 mt(std::random_device{}());
  std::uniform_int_distribution<int> dist;

  constexpr size_t SZ = 1000000;

  std::pair<std::vector<ST>, std::vector<TP>> p;
  auto& s = p.first;
  auto& d = p.second;
  s.reserve(SZ);
  d.reserve(SZ);

  for(size_t i = 0; i < SZ; i++) {
    s.emplace_back();
    auto& sb = s.back();
    sb.X = dist(mt);
    sb.Y = dist(mt);
    sb.Cost = sb.X * sb.Y;
    sb.Label = std::to_string(sb.Cost);

    d.emplace_back(std::tie(sb.X, sb.Y, sb.Cost, sb.Label));
  }

  return p;
}

int main() {
  Timer timer;

  auto p = generate();
  auto& structs = p.first;
  auto& tuples = p.second;

  timer.reset();
  std::sort(structs.begin(), structs.end());
  double stSecs = timer.getElapsedSeconds();

  timer.reset();
  std::sort(tuples.begin(), tuples.end());
  double tpSecs = timer.getElapsedSeconds();

  std::cout << "Structs took " << stSecs << " seconds.\nTuples took " << tpSecs << " seconds.\n";

  std::cin.get();
}
Khatharr
quelle
Danke dafür. Ich bemerkte , dass , wenn sie mit optimierten -O3, tuplesweniger Zeit in Anspruch nahm als structs.
Simog
3

Nun, eine POD-Struktur kann oft (ab) beim Lesen und Serialisieren von zusammenhängenden Chunks auf niedriger Ebene verwendet werden. Ein Tupel ist in bestimmten Situationen möglicherweise optimierter und unterstützt, wie Sie sagten, mehr Funktionen.

Verwenden Sie, was für die Situation besser geeignet ist, es gibt keine allgemeine Präferenz. Ich denke (aber ich habe es nicht bewertet), dass Leistungsunterschiede nicht signifikant sind. Das Datenlayout ist höchstwahrscheinlich nicht kompatibel und implementierungsspezifisch.

orlp
quelle
3

Was die "generische Funktion" betrifft, verdient Boost.Fusion etwas Liebe ... und insbesondere BOOST_FUSION_ADAPT_STRUCT .

Rippen von der Seite: ABRACADBRA

namespace demo
{
    struct employee
    {
        std::string name;
        int age;
    };
}

// demo::employee is now a Fusion sequence
BOOST_FUSION_ADAPT_STRUCT(
    demo::employee
    (std::string, name)
    (int, age))

Dies bedeutet, dass alle Fusion-Algorithmen jetzt auf die Struktur anwendbar sind demo::employee.


BEARBEITEN : In Bezug auf den Leistungsunterschied oder die Layoutkompatibilität ist tupledas Layout der Implementierung so definiert, dass es nicht kompatibel ist (und Sie sollten daher nicht zwischen den beiden Darstellungen wechseln), und im Allgemeinen würde ich dank des Leistungsunterschieds (zumindest in Release) keinen Unterschied erwarten Inlining von get<N>.

Matthieu M.
quelle
15
Ich glaube nicht, dass dies die am besten gewählte Antwort ist. Es antwortet nicht einmal auf die Frage. Die Frage ist über tuples und structs, nicht Boost!
Gsamaras
@ G.Samaras: Die Frage betrifft den Unterschied zwischen Tupeln und structinsbesondere die Fülle von Algorithmen zur Manipulation von Tupeln gegen das Fehlen von Algorithmen zur Manipulation von Strukturen (beginnend mit der Iteration über ihre Felder). Diese Antwort zeigt, dass diese Lücke mithilfe von Boost.Fusion geschlossen werden kann, wodurch structso viele Algorithmen wie auf Tupeln verfügbar sind. Ich habe einen kleinen Klappentext zu den genau zwei gestellten Fragen hinzugefügt.
Matthieu M.
2

Ist das Datenlayout auch miteinander kompatibel (austauschbar gegossen)?

Seltsamerweise kann ich keine direkte Antwort auf diesen Teil der Frage sehen.

Die Antwort lautet: nein . Oder zumindest nicht zuverlässig, da das Layout des Tupels nicht spezifiziert ist.

Erstens ist Ihre Struktur ein Standardlayouttyp . Die Reihenfolge, Polsterung und Ausrichtung der Elemente wird durch eine Kombination aus Standard und Plattform-ABI genau definiert.

Wenn ein Tupel ein Standardlayouttyp wäre und wir wüssten, dass die Felder in der Reihenfolge angeordnet sind, in der die Typen angegeben sind, können wir sicher sein, dass sie mit der Struktur übereinstimmen.

Das Tupel wird normalerweise mithilfe der Vererbung auf zwei Arten implementiert: im alten rekursiven Loki / Modern C ++ Design-Stil oder im neueren variadischen Stil. Keiner ist ein Standardlayout-Typ, da beide die folgenden Bedingungen verletzen:

  1. (vor C ++ 14)

    • hat keine Basisklassen mit nicht statischen Datenelementen oder

    • hat keine nicht statischen Datenelemente in der am meisten abgeleiteten Klasse und höchstens eine Basisklasse mit nicht statischen Datenelementen

  2. (für C ++ 14 und höher)

    • Hat alle nicht statischen Datenelemente und Bitfelder in derselben Klasse deklariert (entweder alle in der abgeleiteten oder alle in einer Basis)

da jede Blatt Basisklasse ein einzelnes Tupelelement enthält (NB. eine Einzelelement - Tupel wahrscheinlich ist ein Standard - Layout - Typ, wenn auch nicht sehr nützlichen). Wir wissen also, dass der Standard nicht garantiert, dass das Tupel die gleiche Auffüllung oder Ausrichtung wie die Struktur hat.

Darüber hinaus ist zu beachten, dass das ältere Tupel im rekursiven Stil die Datenelemente im Allgemeinen in umgekehrter Reihenfolge anordnet.

Anekdotisch hat es in der Vergangenheit bei einigen Compilern und Kombinationen von Feldtypen in der Praxis manchmal funktioniert (in einem Fall mit rekursiven Tupeln nach Umkehrung der Feldreihenfolge). Es funktioniert jetzt definitiv nicht zuverlässig (über Compiler, Versionen usw. hinweg) und wurde überhaupt nicht garantiert.

Nutzlos
quelle
1

Es sollte keinen Leistungsunterschied geben (auch keinen unbedeutenden). Zumindest im Normalfall führen sie zum gleichen Speicherlayout. Trotzdem ist das Casting zwischen ihnen wahrscheinlich nicht erforderlich, um zu funktionieren (obwohl ich vermute, dass es eine ziemlich faire Chance gibt, dass dies normalerweise der Fall ist).

Jerry Sarg
quelle
4
Eigentlich denke ich, dass es einen kleinen Unterschied geben könnte. A structmuss jedem Unterobjekt mindestens 1 Byte zuweisen, während ich denke, dass a tuplemit der Optimierung der leeren Objekte davonkommen kann. Auch in Bezug auf Verpackung und Ausrichtung könnte es sein, dass Tupel mehr Spielraum haben.
Matthieu M.
1

Ich habe die Erfahrung gemacht, dass sich mit der Zeit die Funktionalität von Typen (wie POD-Strukturen) einschleicht, die früher reine Dateninhaber waren. Dinge wie bestimmte Modifikationen, die keine Insiderkenntnisse der Daten, die Aufrechterhaltung von Invarianten usw. erfordern sollten.

Das ist eine gute Sache; Es ist die Grundlage der Objektorientierung. Dies ist der Grund, warum C mit Klassen erfunden wurde. Die Verwendung reiner Datensammlungen wie Tupel ist für eine solche logische Erweiterung nicht offen. Strukturen sind. Deshalb würde ich mich fast immer für Strukturen entscheiden.

Dies hängt damit zusammen, dass Tupel wie alle "offenen Datenobjekte" das Paradigma des Versteckens von Informationen verletzen. Sie können das später nicht ändern, ohne den Tupelgroßhandel wegzuwerfen. Mit einer Struktur können Sie schrittweise zu Zugriffsfunktionen übergehen.

Ein weiteres Problem ist die Typensicherheit und der selbstdokumentierende Code. Wenn Ihre Funktion ein Objekt vom Typ empfängt inbound_telegramoder location_3Des klar ist; Wenn es ein erhält unsigned char *oder tuple<double, double, double>nicht: Das Telegramm könnte ausgehend sein, und das Tupel könnte eine Übersetzung anstelle eines Ortes sein, oder vielleicht die Mindesttemperaturwerte vom langen Wochenende. Ja, Sie können tippen, um die Absichten klar zu machen, aber das hindert Sie nicht daran, Temperaturen zu überschreiten.

Diese Probleme werden in der Regel bei Projekten wichtig, die eine bestimmte Größe überschreiten. Die Nachteile von Tupeln und die Vorteile aufwändiger Klassen werden nicht sichtbar und sind in kleinen Projekten tatsächlich ein Aufwand. Das Starten mit richtigen Klassen auch für unauffällige kleine Datenaggregate zahlt sich spät aus.

Natürlich wäre eine praktikable Strategie, einen reinen Datenhalter als zugrunde liegenden Datenanbieter für einen Klassen-Wrapper zu verwenden, der Operationen für diese Daten bereitstellt.

Peter - Monica wieder einsetzen
quelle
1

Sorgen Sie sich nicht um Geschwindigkeit oder Layout, das ist Nano-Optimierung und hängt vom Compiler ab, und es gibt nie genug Unterschiede, um Ihre Entscheidung zu beeinflussen.

Sie verwenden eine Struktur für Dinge, die sinnvoll zusammengehören, um ein Ganzes zu bilden.

Sie verwenden ein Tupel für Dinge, die zufällig zusammen sind. Sie können ein Tupel spontan in Ihrem Code verwenden.

gnasher729
quelle
0

Ich weiß, dass es ein altes Thema ist, aber ich bin jetzt dabei, eine Entscheidung über einen Teil meines Projekts zu treffen: Sollte ich den Tupel- oder Strukturweg gehen. Nachdem ich diesen Thread gelesen habe, habe ich einige Ideen.

  1. Über die Wheaties und den Leistungstest: Bitte beachten Sie, dass Sie normalerweise memcpy, memset und ähnliche Tricks für Strukturen verwenden können. Dies würde die Leistung VIEL besser machen als bei Tupeln.

  2. Ich sehe einige Vorteile in Tupeln:

    • Sie können Tupel verwenden, um eine Sammlung von Variablen aus Funktion oder Methode zurückzugeben und eine Anzahl von Typen zu verringern, die Sie verwenden.
    • Basierend auf der Tatsache, dass Tupel <, ==,> Operatoren vordefiniert hat, können Sie Tupel auch als Schlüssel in map oder hash_map verwenden. Dies ist wesentlich kostengünstiger als die Struktur, in der Sie diese Operatoren implementieren müssen.

Ich habe das Web durchsucht und schließlich diese Seite erreicht: https://arne-mertz.de/2017/03/smelly-pair-tuple/

Generell stimme ich einer abschließenden Schlussfolgerung von oben zu.

Tom K.
quelle
1
Das klingt eher nach dem, woran Sie arbeiten, und nicht nach einer Antwort auf diese spezielle Frage, oder?
Dieter Meemken
Nichts hält Sie davon ab, memcpy mit Tupeln zu verwenden.
Peter - Monica