Tupla de C ++ frente a estructura

96

¿Hay alguna diferencia entre usar std::tupleay solo de datos struct?

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

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

Por lo que he encontrado en línea, descubrí que hay dos diferencias principales: el structes más legible, mientras que el tupletiene muchas funciones genéricas que se pueden usar. ¿Debería haber alguna diferencia de rendimiento significativa? Además, ¿el diseño de datos es compatible entre sí (emitidos indistintamente)?

Alex Koay
fuente
Acabo de comentar que me había olvidado de la pregunta del reparto : la implementación de la tupleimplementación está definida, por lo tanto, depende de su implementación. Personalmente, no contaría con eso.
Matthieu M.

Respuestas:

32

Tenemos una discusión similar sobre tupla y estructura y escribo algunos puntos de referencia sencillos con la ayuda de uno de mis colegas para identificar las diferencias en términos de rendimiento entre tupla y estructura. Primero comenzamos con una estructura predeterminada y 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>;

Luego usamos Celero para comparar el desempeño de nuestra estructura y tupla simples. A continuación se muestra el código de referencia y los resultados de rendimiento recopilados con gcc-4.9.2 y 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);
}

Resultados de rendimiento recopilados 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.

Y los resultados de rendimiento recopilados con 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.

De los resultados anteriores podemos ver claramente que

  • La tupla es más rápida que una estructura predeterminada

  • El producto binario por clang tiene un rendimiento superior al de gcc. clang-vs-gcc no es el propósito de esta discusión, así que no voy a profundizar en los detalles.

Todos sabemos que escribir un operador == o <o> para cada definición de estructura será una tarea dolorosa y con muchos errores. Reemplace nuestro comparador personalizado usando std :: tie y vuelva a ejecutar nuestro punto de referencia.

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.

Ahora podemos ver que usar std :: tie hace que nuestro código sea más elegante y es más difícil cometer errores, sin embargo, perderemos aproximadamente un 1% de rendimiento. Me quedaré con la solución std :: tie por ahora, ya que también recibo una advertencia sobre la comparación de números de punto flotante con el comparador personalizado.

Hasta ahora no hemos tenido ninguna solución para hacer que nuestro código de estructura se ejecute más rápido. Echemos un vistazo a la función de intercambio y vuelva a escribirla para ver si podemos obtener algún rendimiento:

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

Resultados de rendimiento recopilados 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 |    176308.80000 |            5.67 | 
Sort            | tuple           | Null            |              10 |               5 |         1.02699 |    181067.60000 |            5.52 | 
Complete.

Y los resultados de rendimiento recopilados con 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.

Ahora nuestra estructura es ligeramente más rápida que la de una tupla ahora (alrededor del 3% con clang y menos del 1% con gcc), sin embargo, necesitamos escribir nuestra función de intercambio personalizada para todas nuestras estructuras.

colgado
fuente
24

Si está usando varias tuplas diferentes en su código, puede salirse con la suya condensando la cantidad de functores que está usando. Digo esto porque a menudo he usado las siguientes formas de functores:

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

Esto puede parecer excesivo, pero para cada lugar dentro de la estructura tendría que crear un objeto functor completamente nuevo usando una estructura, pero para una tupla, simplemente cambio N. Mejor que eso, puedo hacer esto para cada tupla en lugar de crear un functor completamente nuevo para cada estructura y para cada variable miembro. Si tengo N estructuras con M variables miembro que NxM functors necesitaría crear (en el peor de los casos) que se pueden condensar en un pedacito de código.

Naturalmente, si va a seguir el camino de Tuple, también necesitará crear Enums para trabajar con ellos:

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

y boom, tu código es completamente legible:

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

porque se describe a sí mismo cuando desea obtener los elementos que contiene.

Wheaties
fuente
8
Uh ... C ++ tiene punteros de funciones, por lo que template <typename C, typename T, T C::*> struct struct_less { template <typename C> bool operator()(C const&, C const&) const; };debería ser posible. Deletrearlo es un poco menos conveniente, pero solo se escribe una vez.
Matthieu M.
17

Tuple tiene incorporado por defecto (para == y! = Compara cada elemento, para <. <= ... compara primero, si lo mismo compara segundo ...) comparadores: http://en.cppreference.com/w/ cpp / utility / tuple / operator_cmp

editar: como se indica en el comentario, el operador de nave espacial C ++ 20 le brinda una manera de especificar esta funcionalidad con una (fea, pero aún solo una) línea de código.

NoSenseEtAl
fuente
1
En C ++ 20, esto se solucionó con un mínimo de repetición utilizando el operador de la nave espacial .
John McFarlane
6

Bueno, aquí hay un punto de referencia que no construye un montón de tuplas dentro del operador de estructura == (). Resulta que el uso de tuplas tiene un impacto bastante significativo en el rendimiento, como cabría esperar dado que no hay ningún impacto en el rendimiento por el uso de POD. (El solucionador de direcciones encuentra el valor en la canalización de instrucciones antes de que la unidad lógica lo vea).

Resultados comunes de ejecutar esto en mi máquina con VS2015CE usando la configuración predeterminada de 'Versión':

Structs took 0.0814905 seconds.
Tuples took 0.282463 seconds.

Por favor juegue con él hasta que esté satisfecho.

#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
fuente
Gracias por esto. Noté que cuando se optimizó con -O3, tuplestomó menos tiempo que structs.
Simog
3

Bueno, una estructura POD a menudo se puede (ab) usar en lectura y serialización de fragmentos contiguos de bajo nivel. Una tupla podría estar más optimizada en determinadas situaciones y admitir más funciones, como dijiste.

Use lo que sea más apropiado para la situación, no hay preferencia general. Creo (pero no lo he comparado) que las diferencias de rendimiento no serán significativas. Lo más probable es que el diseño de los datos no sea compatible y específico de la implementación.

orlp
fuente
3

En cuanto a la "función genérica", Boost.Fusion merece un poco de amor ... y especialmente BOOST_FUSION_ADAPT_STRUCT .

Extrayendo de la página: 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))

Esto significa que todos los algoritmos de Fusion ahora son aplicables a la estructura demo::employee.


EDITAR : Con respecto a la diferencia de rendimiento o la compatibilidad del diseño, tupleel diseño está definido por la implementación, por lo que no es compatible (y, por lo tanto, no debe lanzar entre ninguna de las representaciones) y, en general, no esperaría ninguna diferencia en cuanto al rendimiento (al menos en la versión) gracias a la forro de get<N>.

Matthieu M.
fuente
16
No creo que esta sea la respuesta más votada. Ni siquiera responde a la pregunta. La pregunta es sobre tuples y structs, ¡no sobre impulso!
gsamaras
@ G.Samaras: La pregunta es sobre la diferencia entre tuplas y struct, en particular, la abundancia de algoritmos para manipular tuplas frente a la ausencia de algoritmos para manipular estructuras (comenzando por iterar sobre sus campos). Esta respuesta muestra que esta brecha se puede salvar usando Boost.Fusion, trayendo a structs tantos algoritmos como tuplas. Agregué una pequeña propaganda sobre las dos preguntas exactas.
Matthieu M.
3

Además, ¿el diseño de datos es compatible entre sí (emitidos indistintamente)?

Curiosamente, no veo una respuesta directa a esta parte de la pregunta.

La respuesta es: no . O al menos no de forma fiable, ya que el diseño de la tupla no está especificado.

En primer lugar, su estructura es un tipo de diseño estándar . El orden, el relleno y la alineación de los miembros están bien definidos por una combinación del estándar y la ABI de su plataforma.

Si una tupla fuera un tipo de diseño estándar y supiéramos que los campos se distribuyeron en el orden en que se especifican los tipos, podríamos tener cierta confianza en que coincidiría con la estructura.

La tupla se implementa normalmente usando herencia, en una de dos formas: el antiguo estilo recursivo Loki / Modern C ++ Design, o el nuevo estilo variadic. Tampoco es un tipo de diseño estándar, porque ambos violan las siguientes condiciones:

  1. (antes de C ++ 14)

    • no tiene clases base con miembros de datos no estáticos, o

    • no tiene miembros de datos no estáticos en la clase más derivada y como máximo una clase base con miembros de datos no estáticos

  2. (para C ++ 14 y posteriores)

    • Tiene todos los miembros de datos no estáticos y campos de bits declarados en la misma clase (ya sea todos en el derivado o todos en alguna base)

dado que cada clase base hoja contiene un solo elemento de tupla (NB. Una tupla de un solo elemento probablemente sea un tipo de diseño estándar, aunque no muy útil). Entonces, sabemos que el estándar no garantiza que la tupla tenga el mismo relleno o alineación que la estructura.

Además, vale la pena señalar que la tupla de estilo recursivo anterior generalmente colocará los miembros de datos en orden inverso.

Como anécdota, a veces ha funcionado en la práctica para algunos compiladores y combinaciones de tipos de campos en el pasado (en un caso, usando tuplas recursivas, después de invertir el orden de los campos). Definitivamente no funciona de manera confiable (en compiladores, versiones, etc.) ahora, y nunca se garantizó en primer lugar.

Inútil
fuente
1

No debería haber una diferencia de rendimiento (incluso una insignificante). Al menos en el caso normal, darán como resultado el mismo diseño de memoria. No obstante, es probable que no se requiera que el casting entre ellos funcione (aunque supongo que hay muchas posibilidades de que normalmente lo haga).

Jerry Coffin
fuente
4
De hecho, creo que puede haber una pequeña diferencia. A structdebe asignar al menos 1 byte para cada subobjeto, mientras que creo que tuplepuede salirse con la suya optimizando los objetos vacíos. Además, con respecto al empaquetado y la alineación, es posible que las tuplas tengan más margen de maniobra.
Matthieu M.
1

Mi experiencia es que, con el tiempo, la funcionalidad comienza a aumentar sigilosamente en tipos (como estructuras POD) que solían ser portadores de datos puros. Cosas como ciertas modificaciones que no deberían requerir un conocimiento interno de los datos, mantener invariantes, etc.

Esa es una buena cosa; es la base de la orientación a objetos. Es la razón por la que se inventó C con clases. El uso de colecciones de datos puros como tuplas no está abierto a tal extensión lógica; las estructuras son. Es por eso que casi siempre optaría por estructuras.

Relacionado es que, como todos los "objetos de datos abiertos", las tuplas violan el paradigma de ocultación de información. No puedes cambiar eso más tarde sin tirar la tupla al por mayor. Con una estructura, puede avanzar gradualmente hacia las funciones de acceso.

Otro problema es la seguridad de tipos y el código autodocumentado. Si su función recibe un objeto de tipo inbound_telegramo location_3Destá claro; si recibe un unsigned char *otuple<double, double, double> no: el telegrama podría ser saliente y la tupla podría ser una traducción en lugar de una ubicación, o quizás las lecturas de temperatura mínima del fin de semana largo. Sí, puede escribir def para dejar claras las intenciones, pero eso en realidad no le impide pasar las temperaturas.

Estos temas tienden a cobrar importancia en proyectos que exceden un cierto tamaño; las desventajas de las tuplas y las ventajas de las clases elaboradas se vuelven invisibles y, de hecho, son una sobrecarga en proyectos pequeños. Comenzar con clases adecuadas, incluso para pequeños agregados de datos discretos, paga dividendos tardíos.

Por supuesto, una estrategia viable sería utilizar un contenedor de datos puro como proveedor de datos subyacente para un contenedor de clase que proporciona operaciones sobre esos datos.

Peter - Reincorporar a Monica
fuente
1

No se preocupe por la velocidad o el diseño, eso es nano-optimización y depende del compilador, y nunca hay suficiente diferencia para influir en su decisión.

Utiliza una estructura para las cosas que pertenecen juntas de manera significativa para formar un todo.

Utiliza una tupla para las cosas que están juntas por coincidencia. Puede utilizar una tupla de forma espontánea en su código.

gnasher729
fuente
1

A juzgar por otras respuestas, las consideraciones de rendimiento son mínimas en el mejor de los casos.

Así que realmente debería reducirse a la practicidad, legibilidad y facilidad de mantenimiento. Y structgeneralmente es mejor porque crea tipos que son más fáciles de leer y comprender.

A veces, un std::tuple(o incluso std::pair) puede ser necesario para tratar el código de una manera muy genérica. Por ejemplo, algunas operaciones relacionadas con paquetes de parámetros variados serían imposibles sin algo como std::tuple. std::tiees un gran ejemplo de cuándo se std::tuplepuede mejorar el código (antes de C ++ 20).

Pero en cualquier lugar donde pueda usar un struct, probablemente debería usar un struct. Otorgará significado semántico a los elementos de su tipo. Eso es invaluable para comprender y usar el tipo. A su vez, esto puede ayudar a evitar errores tontos:

// 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;
John McFarlane
fuente
0

Sé que es un tema antiguo, sin embargo, ahora estoy a punto de tomar una decisión sobre parte de mi proyecto: ¿debo seguir el camino de la tupla o el de la estructura? Después de leer este hilo tengo algunas ideas.

  1. Acerca de los wheaties y la prueba de rendimiento: tenga en cuenta que generalmente puede usar memcpy, memset y trucos similares para estructuras. Esto haría que el rendimiento fuera MUCHO mejor que el de las tuplas.

  2. Veo algunas ventajas en las tuplas:

    • Puede usar tuplas para devolver una colección de variables de función o método y disminuir la cantidad de tipos que usa.
    • Con base en el hecho de que tuple tiene operadores <, ==,> predefinidos, también puede usar tuple como clave en el mapa o hash_map, que es mucho más rentable que la estructura donde necesita implementar estos operadores.

He buscado en la web y finalmente llegué a esta página: https://arne-mertz.de/2017/03/smelly-pair-tuple/

Generalmente estoy de acuerdo con una conclusión final de arriba.

Tom K
fuente
1
Esto suena más a lo que está trabajando y no a una respuesta a esa pregunta específica, ¿o?
Dieter Meemken
Nada le impide usar memcpy con tuplas.
Peter - Restablece a Monica el