¿Hay alguna diferencia entre usar std::tuple
ay 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 struct
es más legible, mientras que el tuple
tiene 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)?
tuple
implementación está definida, por lo tanto, depende de su implementación. Personalmente, no contaría con eso.Respuestas:
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.
fuente
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.
fuente
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.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.
fuente
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(); }
fuente
-O3
,tuples
tomó menos tiempo questructs
.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.
fuente
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,
tuple
el 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 deget<N>
.fuente
tuple
s ystruct
s, ¡no sobre impulso!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 astruct
s tantos algoritmos como tuplas. Agregué una pequeña propaganda sobre las dos preguntas exactas.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:
(antes de C ++ 14)
(para C ++ 14 y posteriores)
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.
fuente
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).
fuente
struct
debe asignar al menos 1 byte para cada subobjeto, mientras que creo quetuple
puede 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.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_telegram
olocation_3D
está claro; si recibe ununsigned 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.
fuente
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.
fuente
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
struct
generalmente es mejor porque crea tipos que son más fáciles de leer y comprender.A veces, un
std::tuple
(o inclusostd::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 comostd::tuple
.std::tie
es un gran ejemplo de cuándo sestd::tuple
puede mejorar el código (antes de C ++ 20).Pero en cualquier lugar donde pueda usar un
struct
, probablemente debería usar unstruct
. 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;
fuente
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.
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.
Veo algunas ventajas en las tuplas:
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.
fuente