web-dev-qa-db-fra.com

C++ Tuple vs Struct

Existe-t-il une différence entre utiliser un std::Tuple et une struct de données uniquement?

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

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

D'après ce que j'ai trouvé en ligne, il y a deux différences majeures: la struct est plus lisible, alors que la Tuple a de nombreuses fonctions génériques pouvant être utilisées . Devrait-il y avoir une différence de performance significative? la mise en page des données est-elle compatible les unes avec les autres (casté de façon interchangeable)?

78
Alex Koay

Si vous utilisez plusieurs nuplets différents dans votre code, vous pouvez vous contenter de condenser le nombre de foncteurs que vous utilisez. Je dis cela parce que j'ai souvent utilisé les types de foncteurs suivants:

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

Cela peut paraître exagéré, mais pour chaque endroit de la structure, je devrais créer un tout nouvel objet foncteur en utilisant une structure, mais pour un Tuple, je change simplement de N. Mieux que cela, je peux le faire pour chaque tuple au lieu de créer un nouveau foncteur pour chaque structure et pour chaque variable membre. Si j’ai N structures avec M variables membres que les foncteurs NxM, j’aurais besoin de créer (pire scénario) qui puisse être condensé en un peu de code.

Naturellement, si vous optez pour la méthode Tuple, vous devrez également créer Enums pour pouvoir travailler avec eux:

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

et boum, votre code est complètement lisible:

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

car il se décrit quand vous voulez obtenir les éléments qu'il contient.

21
wheaties

Nous avons une discussion similaire à propos de Tuple et de struct et j’écris quelques repères simples avec l’aide d’un de mes collègues pour identifier les différences de performance entre Tuple et struct. Nous commençons par une structure par défaut et un tuple. 

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

Nous utilisons ensuite Celero pour comparer les performances de nos structures simples et Tuple. Vous trouverez ci-dessous le code de référence et les résultats de performance collectés à l'aide de gcc-4.9.2 et de 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);
}

Résultats de performance collectés avec 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.

Et les résultats de performance collectés avec 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.

Les résultats ci-dessus montrent clairement que 

  • Tuple est plus rapide qu'une structure par défaut  

  • Les produits binaires de Clang ont des performances supérieures à celles de gcc. clang-vs-gcc n’est pas l’objet de cette discussion, je ne vais donc pas entrer dans les détails. 

Nous savons tous que l'écriture d'un opérateur == ou <ou> pour chaque définition de structure sera une tâche pénible et fastidieuse. Laissons remplacer notre comparateur personnalisé à l’aide de std :: tie et réexécuter notre test. 

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.

Nous pouvons maintenant voir que l'utilisation de std :: tie rend notre code plus élégant et qu'il est plus difficile de se tromper, cependant, nous perdrons environ 1% de performance. Je vais rester avec la solution std :: tie pour l'instant car je reçois également un avertissement concernant la comparaison de nombres en virgule flottante avec le comparateur personnalisé. 

Jusqu'à présent, nous n'avons pas encore de solution pour rendre notre code struct plus rapide. Jetons un coup d'œil à la fonction swap et récrivons-la pour voir si nous pouvons obtenir des performances:

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

Résultats de performance collectés à l'aide de 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.

Et les résultats de performance collectés avec 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.

Maintenant, notre structure est légèrement plus rapide que celle d'un Tuple (environ 3% avec clang et moins de 1% avec gcc), cependant, nous avons besoin d'écrire notre fonction d'échange personnalisée pour toutes nos structures.

17
hungptit

Tuple a construit par défaut (pour == et! = Il compare chaque élément, pour <. <= ... compare en premier, si identique compare en second ...) comparateurs: http://fr.cppreference.com/w/cpp/utility/Tuple/operator_cmp

13
NoSenseEtAl

Eh bien, voici un point de repère qui ne construit pas un tas de nuplets dans l’opérateur struct == (). Il s'avère que l'utilisation de Tuple a un impact assez important sur les performances, comme on pouvait s'y attendre, étant donné que l'utilisation des POD n'a aucun impact sur les performances. (Le résolveur d'adresses trouve la valeur dans le pipeline d'instructions avant même que l'unité logique ne la voie.)

Les résultats courants de l’exécution de cette opération sur ma machine avec VS2015CE avec les paramètres par défaut de "Release":

Structs took 0.0814905 seconds.
Tuples took 0.282463 seconds.

S'il vous plaît singe avec elle jusqu'à ce que vous êtes satisfait.

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

Eh bien, une structure POD peut souvent être (ab) utilisée dans la lecture et la sérialisation de blocs contigus de bas niveau. Un tuple peut être plus optimisé dans certaines situations et prendre en charge davantage de fonctions, comme vous l'avez dit.

Utilisez ce qui convient le mieux à la situation, il n'y a pas de préférence générale. Je pense (mais je ne l'ai pas évalué) que les différences de performances ne seront pas significatives. La structure des données est probablement incompatible et spécifique à la mise en œuvre.

3
orlp

En ce qui concerne la "fonction générique", Boost.Fusion mérite un peu d'amour ... et surtout BOOST_FUSION_ADAPT_STRUCT .

Extraction de la page: 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))

Cela signifie que tous les algorithmes de Fusion sont maintenant applicables à la struct demo::employee.


EDIT: En ce qui concerne la différence de performance ou la compatibilité de la mise en page, la mise en page de Tuple est définie de sorte qu'elle n'est pas compatible Release) grâce à l’insertion de get<N>.

3
Matthieu M.

Il ne devrait pas y avoir de différence de performance (même insignifiante). Au moins dans les cas normaux, ils entraîneront le même agencement de mémoire. Néanmoins, il n’est probablement pas nécessaire de jouer entre eux.

1
Jerry Coffin

Je sais que c’est un vieux thème, mais je suis maintenant sur le point de prendre une décision concernant une partie de mon projet: devrais-je choisir la méthode Tuple-way ou struct-way… .. Après avoir lu ce fil, j’ai quelques idées. 
1. À propos des tests et des tests de performance - veuillez noter que vous pouvez généralement utiliser memcpy, memset et des astuces similaires pour les structs. Cela rendrait la performance BEAUCOUP plus grande que les tupples.
2. Je vois quelques avantages dans les tuples:
a) vous pouvez utiliser des n-uplets pour renvoyer une collection de variables de fonction ou de méthode et réduire le nombre de types que vous utilisez,
b) En vous basant sur le fait que Tuple a prédéfini les opérateurs <, ==,>, vous pouvez également utiliser Tuple en tant que clé dans map ou hash_map, qui est beaucoup plus économique que la structure dans laquelle vous devez implémenter ces opérateurs.
J'ai commencé à creuser le www et suis finalement parvenu à cette page:
https://arne-mertz.de/2017/03/smelly-pair-Tuple/
En général, je suis d’accord avec une conclusion finale tirée d’en haut.

0
Tom K