Tupla C ++ vs Struct


96

C'è qualche differenza tra l'utilizzo di un std::tuplee un solo dati struct?

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

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

Da quello che ho trovato online, ho scoperto che ci sono due differenze principali: structè più leggibile, mentre tupleha molte funzioni generiche che possono essere utilizzate. Dovrebbe esserci una differenza significativa nelle prestazioni? Inoltre, il layout dei dati è compatibile tra loro (cast in modo intercambiabile)?


Ho appena notato che mi ero dimenticato della domanda cast : l'implementazione dell'implementazione tupleè definita, quindi dipende dalla tua implementazione. Personalmente, vorrei non contare su di esso.
Matthieu M.

Risposte:


32

Abbiamo una discussione simile su tuple e struct e scrivo alcuni semplici benchmark con l'aiuto di un mio collega per identificare le differenze in termini di prestazioni tra tuple e struct. Iniziamo prima con una struttura predefinita e una tupla.

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>;

Quindi usiamo Celero per confrontare le prestazioni della nostra semplice struttura e tupla. Di seguito è riportato il codice di benchmark ei risultati delle prestazioni raccolti utilizzando gcc-4.9.2 e clang-4.0.0:

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);
}

Risultati delle prestazioni raccolti con clang-4.0.0

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.

E i risultati delle prestazioni raccolti utilizzando gcc-4.9.2

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.

Dai risultati di cui sopra possiamo vederlo chiaramente

  • Tuple è più veloce di una struttura predefinita

  • Il prodotto binario di clang ha prestazioni superiori a quelle di gcc. clang-vs-gcc non è lo scopo di questa discussione, quindi non mi immergerò nei dettagli.

Sappiamo tutti che scrivere un operatore == o <o> per ogni singola definizione di struttura sarà un'attività dolorosa e buggata. Sostituiamo il nostro comparatore personalizzato usando std :: tie e rieseguiamo il nostro benchmark.

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.

Ora possiamo vedere che l'uso di std :: tie rende il nostro codice più elegante ed è più difficile sbagliare, tuttavia, perderemo circa l'1% delle prestazioni. Per ora rimarrò con la soluzione std :: tie poiché ricevo anche un avviso sul confronto dei numeri in virgola mobile con il comparatore personalizzato.

Fino ad ora non abbiamo ancora alcuna soluzione per rendere più veloce il nostro codice struct. Diamo un'occhiata alla funzione di scambio e riscrivila per vedere se possiamo ottenere prestazioni:

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);
    }
};

Risultati delle prestazioni raccolti utilizzando clang-4.0.0

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.

E i risultati delle prestazioni raccolti utilizzando gcc-4.9.2

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.

Ora la nostra struttura è leggermente più veloce di quella di una tupla ora (circa il 3% con clang e meno dell'1% con gcc), tuttavia, abbiamo bisogno di scrivere la nostra funzione di scambio personalizzata per tutte le nostre strutture.


24

Se stai usando diverse tuple diverse nel tuo codice, puoi farla franca condensando il numero di funtori che stai usando. Dico questo perché ho spesso usato le seguenti forme di funtori:

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);
    }
};

Potrebbe sembrare eccessivo, ma per ogni posizione all'interno della struttura dovrei creare un oggetto funtore completamente nuovo usando una struttura, ma per una tupla, cambio N. Meglio di così, posso farlo per ogni singola tupla invece di creare un funtore completamente nuovo per ogni struttura e per ogni variabile membro. Se ho strutture N con variabili membro M che i funtori NxM avrei bisogno di creare (scenario peggiore) che può essere condensato in un po 'di codice.

Naturalmente, se hai intenzione di seguire la modalità Tuple, dovrai anche creare Enum per lavorare con loro:

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

e boom, il tuo codice è completamente leggibile:

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

perché descrive se stesso quando vuoi ottenere gli elementi in esso contenuti.


8
Uh ... C ++ ha puntatori a funzioni, quindi template <typename C, typename T, T C::*> struct struct_less { template <typename C> bool operator()(C const&, C const&) const; };dovrebbe essere possibile. Spiegarlo è leggermente meno conveniente, ma viene scritto solo una volta.
Matthieu M.

17

Tuple ha dei comparatori predefiniti (per == e! = Confronta ogni elemento, per <. <= ... confronta prima, se stesso confronta il secondo ...): http://en.cppreference.com/w/ cpp / utility / tuple / operator_cmp

modifica: come notato nel commento, l'operatore di navicella spaziale C ++ 20 ti dà un modo per specificare questa funzionalità con una (brutta, ma sempre solo una) riga di codice.


1
In C ++ 20, questo è stato risolto con un boilerplate minimo utilizzando l'operatore di astronave .
John McFarlane,

6

Bene, ecco un benchmark che non costruisce un gruppo di tuple all'interno dell'operatore struct == (). Risulta che l'uso della tupla ha un impatto sulle prestazioni piuttosto significativo, come ci si aspetterebbe dato che non c'è alcun impatto sulle prestazioni dall'uso dei POD. (Il risolutore di indirizzi trova il valore nella pipeline di istruzioni prima che l'unità logica lo veda.)

Risultati comuni dall'esecuzione di questo sulla mia macchina con VS2015CE utilizzando le impostazioni di "Release" predefinite:

Structs took 0.0814905 seconds.
Tuples took 0.282463 seconds.

Per favore scimmia con esso finché non sei soddisfatto.

#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();
}

Grazie per questo. Ho notato che, se ottimizzato con -O3, tuplesrichiedeva meno tempo di structs.
Simog

3

Bene, una struttura POD può spesso essere (ab) usata nella lettura e serializzazione di blocchi contigui di basso livello. Una tupla potrebbe essere più ottimizzata in determinate situazioni e supportare più funzioni, come hai detto.

Usa tutto ciò che è più appropriato per la situazione, non ci sono preferenze generali. Penso (ma non l'ho confrontato) che le differenze di prestazioni non saranno significative. Il layout dei dati è molto probabilmente non compatibile e specifico per l'implementazione.


3

Per quanto riguarda la "funzione generica", Boost.Fusion merita un po 'di amore ... e soprattutto BOOST_FUSION_ADAPT_STRUCT .

Strappo dalla pagina: 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))

Ciò significa che tutti gli algoritmi di Fusion sono ora applicabili alla struttura demo::employee.


MODIFICA : per quanto riguarda la differenza di prestazioni o la compatibilità del layout, tupleil layout di è definito dall'implementazione in modo non compatibile (e quindi non dovresti eseguire il cast tra nessuna delle due rappresentazioni) e in generale non mi aspetterei alcuna differenza in termini di prestazioni (almeno nella versione) grazie al inlining di get<N>.


16
Non credo che questa sia la risposta più votata. Non risponde nemmeno alla domanda. La domanda è di circa tuples e structs, senza aumentare!
gsamaras

@ G.Samaras: La domanda riguarda la differenza tra le tuple e struct, in particolare, l'abbondanza di algoritmi per manipolare le tuple contro l'assenza di algoritmi per manipolare le strutture (iniziando dall'iterazione sui suoi campi). Questa risposta mostra che questo divario può essere colmato usando Boost.Fusion, portando a structs tanti algoritmi quante sono le tuple. Ho aggiunto un piccolo blurb sulle due domande esatte poste.
Matthieu M.

3

Inoltre, il layout dei dati è compatibile tra loro (trasmessi in modo intercambiabile)?

Stranamente non riesco a vedere una risposta diretta a questa parte della domanda.

La risposta è: no . O almeno non in modo affidabile, poiché il layout della tupla non è specificato.

In primo luogo, la tua struttura è un tipo di layout standard . L'ordinamento, il riempimento e l'allineamento dei membri sono ben definiti da una combinazione dello standard e dell'ABI della piattaforma.

Se una tupla fosse un tipo di layout standard e sapessimo che i campi erano disposti nell'ordine in cui sono specificati i tipi, potremmo avere una certa sicurezza che corrisponderebbe alla struttura.

La tupla viene normalmente implementata usando l'ereditarietà, in uno dei due modi: il vecchio stile ricorsivo di Loki / Modern C ++ Design o il più recente stile variadico. Nessuno dei due è un tipo di layout standard, perché entrambi violano le seguenti condizioni:

  1. (prima di C ++ 14)

    • non ha classi base con membri dati non statici o

    • non ha membri dati non statici nella classe più derivata e al massimo una classe base con membri dati non statici

  2. (per C ++ 14 e versioni successive)

    • Ha tutti i membri di dati non statici e i campi di bit dichiarati nella stessa classe (tutti nel derivato o tutti in qualche base)

poiché ogni classe base foglia contiene un singolo elemento di tupla (NB. una tupla a singolo elemento probabilmente è un tipo di layout standard, anche se non molto utile). Quindi, sappiamo che lo standard non garantisce che la tupla abbia lo stesso riempimento o allineamento della struttura.

Inoltre, vale la pena notare che la vecchia tupla in stile ricorsivo generalmente disporrà i membri dei dati in ordine inverso.

Aneddoticamente, a volte ha funzionato in pratica per alcuni compilatori e combinazioni di tipi di campo in passato (in un caso, utilizzando tuple ricorsive, dopo aver invertito l'ordine dei campi). Sicuramente non funziona in modo affidabile (tra compilatori, versioni ecc.) Ora e non è mai stato garantito in primo luogo.


1

Non dovrebbe esserci una differenza di prestazioni (anche insignificante). Almeno nel caso normale, risulteranno nello stesso layout di memoria. Tuttavia, il casting tra di loro probabilmente non è necessario per funzionare (anche se immagino che ci siano buone probabilità che normalmente lo faccia).


4
In realtà penso che potrebbe esserci una piccola differenza. A structdeve allocare almeno 1 byte per ogni sottooggetto mentre penso che a tuplepuò farla franca ottimizzando gli oggetti vuoti. Inoltre, per quanto riguarda il compattamento e l'allineamento, potrebbe essere che le tuple abbiano più margine di manovra.
Matthieu M.

1

La mia esperienza è che nel tempo la funzionalità inizia a insinuarsi su tipi (come le strutture POD) che erano puri titolari di dati. Cose come certe modifiche che non dovrebbero richiedere una conoscenza interna dei dati, il mantenimento di invarianti ecc.

Questa è una buona cosa; è il fondamento dell'orientamento agli oggetti. È il motivo per cui è stato inventato il C con classi. L'uso di raccolte di dati puri come le tuple non è aperto a tale estensione logica; gli struct sono. Ecco perché opterei quasi sempre per gli struct.

È correlato che, come tutti gli "oggetti dati aperti", le tuple violano il paradigma che nasconde le informazioni. Non puoi cambiarlo in seguito senza buttare via la tupla all'ingrosso. Con una struttura ci si può muovere gradualmente verso le funzioni di accesso.

Un altro problema è la sicurezza dei tipi e il codice auto-documentante. Se la tua funzione riceve un oggetto di tipo inbound_telegramo location_3Dè chiaro; se riceve un unsigned char *otuple<double, double, double> non lo è: il telegramma potrebbe essere in uscita e la tupla potrebbe essere una traduzione invece di una posizione, o forse le letture della temperatura minima del lungo weekend. Sì, puoi typedef per rendere chiare le intenzioni, ma ciò non ti impedisce in realtà di superare le temperature.

Questi problemi tendono a diventare importanti nei progetti che superano una certa dimensione; gli svantaggi delle tuple ei vantaggi delle classi elaborate diventano non visibili e in effetti sono un sovraccarico nei piccoli progetti. Iniziare con classi adeguate anche per piccoli aggregati di dati poco appariscenti paga i dividendi in ritardo.

Ovviamente una strategia praticabile sarebbe quella di utilizzare un titolare di dati puro come fornitore di dati sottostante per un wrapper di classe che fornisce operazioni su quei dati.


1

Non preoccuparti della velocità o del layout, questa è la nano-ottimizzazione e dipende dal compilatore e non c'è mai abbastanza differenza per influenzare la tua decisione.

Usi una struttura per cose che appartengono in modo significativo insieme per formare un tutto.

Usi una tupla per cose che stanno insieme casualmente. Puoi usare una tupla spontaneamente nel tuo codice.


1

A giudicare da altre risposte, le considerazioni sulle prestazioni sono minime nella migliore delle ipotesi.

Quindi dovrebbe davvero scendere a praticità, leggibilità e manutenibilità. Ed structè generalmente migliore perché crea tipi più facili da leggere e da capire.

A volte, potrebbe essere necessario std::tuple(o addirittura std::pair) trattare il codice in modo altamente generico. Ad esempio, alcune operazioni relative ai pacchetti di parametri variadici sarebbero impossibili senza qualcosa di simile std::tuple. std::tieè un ottimo esempio di quando è std::tuplepossibile migliorare il codice (prima di C ++ 20).

Ma ovunque tu possa usare un struct, probabilmente dovresti usare un struct. Darà un significato semantico agli elementi del tuo tipo. Ciò è inestimabile per comprendere e utilizzare il tipo. A sua volta, questo può aiutare a evitare errori stupidi:

// hard to get wrong; easy to understand
cat.arms = 0;
cat.legs = 4;

// easy to get wrong; hard to understand
std::get<0>(cat) = 0;
std::get<1>(cat) = 4;

0

So che è un vecchio tema, ma ora sto per prendere una decisione su una parte del mio progetto: dovrei andare in tuple-way o in struct-way. Dopo aver letto questo thread ho alcune idee.

  1. Riguardo ai wheaties e al test delle prestazioni: tieni presente che di solito puoi usare memcpy, memset e trucchi simili per gli struct. Ciò renderebbe le prestazioni MOLTO migliori rispetto alle tuple.

  2. Vedo alcuni vantaggi nelle tuple:

    • È possibile utilizzare le tuple per restituire una raccolta di variabili da una funzione o da un metodo e diminuire un numero di tipi utilizzati.
    • Basandosi sul fatto che tuple ha operatori <, ==,> predefiniti, puoi anche usare tuple come chiave in map o hash_map che è molto più conveniente rispetto alla struttura in cui devi implementare questi operatori.

Ho cercato sul web e alla fine sono arrivato a questa pagina: https://arne-mertz.de/2017/03/smelly-pair-tuple/

In generale sono d'accordo con una conclusione finale dall'alto.


1
Questo suona più come quello su cui stai lavorando e non una risposta a quella domanda specifica, o?
Dieter Meemken

Niente ti impedisce di usare memcpy con le tuple.
Peter - Ripristina Monica
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.