A veces se afirma que C++ 11/14 puede aumentar el rendimiento incluso cuando solo se compila código C++ 98. La justificación suele estar en la línea de la semántica de movimiento, ya que en algunos casos los constructores de rvalue se generan automáticamente o ahora forman parte de la STL. Ahora me pregunto si estos casos ya fueron manejados anteriormente por RVO u optimizaciones de compilador similares.
Entonces, mi pregunta es si podría darme un ejemplo real de una pieza de código C++98 que, sin modificaciones, se ejecute más rápido usando un compilador compatible con las características del nuevo lenguaje. Entiendo que no se requiere un compilador conforme al estándar para hacer la elisión de copia y solo por esa razón, la semántica de movimiento puede generar velocidad, pero me gustaría ver un caso menos patológico, por así decirlo.
EDITAR: Para que quede claro, no estoy preguntando si los compiladores nuevos son más rápidos que los compiladores antiguos, sino si hay un código mediante el cual agregar -std = c ++ 14 a las banderas de mi compilador se ejecutaría más rápido (evite las copias, pero si usted puede pensar en cualquier otra cosa además de la semántica de movimiento, también me interesaría)
Conozco 5 categorías generales en las que volver a compilar un compilador C++03 como C++11 puede causar aumentos de rendimiento ilimitados que prácticamente no están relacionados con la calidad de la implementación. Todas estas son variaciones de la semántica de movimiento.
std::vector
redistribuir
struct bar{
std::vector<int> data;
};
std::vector<bar> foo(1);
foo.back().data.push_back(3);
foo.reserve(10); // two allocations and a delete occur in C++03
cada vez que el foo
El búfer se reasigna en C++ 03, se copia cada vector
en bar
.
En C++11, en cambio, mueve el bar::data
s, que es básicamente gratis.
En este caso, esto se basa en optimizaciones dentro del std
envase vector
. En todos los casos a continuación, el uso de std
contenedores es solo porque son objetos C++ que tienen eficiente move
semántica en C++ 11 “automáticamente” cuando actualiza su compilador. Objetos que no lo bloqueen que contengan un std
El contenedor también hereda la mejora automática. move
constructores
falla de NRVO
Cuando falla NRVO (denominado optimización del valor de retorno), en C++03 recurre a la copia, en C++11 recurre al movimiento. Las fallas de NRVO son fáciles:
std::vector<int> foo(int count){
std::vector<int> v; // oops
if (count<=0) return std::vector<int>();
v.reserve(count);
for(int i=0;i<count;++i)
v.push_back(i);
return v;
}
o incluso:
std::vector<int> foo(bool which) {
std::vector<int> a, b;
// do work, filling a and b, using the other for calculations
if (which)
return a;
else
return b;
}
Tenemos tres valores: el valor de retorno y dos valores diferentes dentro de la función. Elision permite que los valores dentro de la función se ‘fusionen’ con el valor devuelto, pero no entre sí. Ambos no se pueden fusionar con el valor devuelto sin fusionarse entre sí.
El problema básico es que la elisión de NRVO es frágil y el código con cambios no está cerca del return
el sitio puede tener repentinamente reducciones masivas de rendimiento en ese lugar sin que se emita ningún diagnóstico. En la mayoría de los casos de falla de NRVO, C ++ 11 termina con un move
mientras que C++03 termina con una copia.
Devolver un argumento de función
La elisión también es imposible aquí:
std::set<int> func(std::set<int> in){
return in;
}
en C++11 esto es barato: en C++03 no hay forma de evitar la copia. Los argumentos de las funciones no se pueden elidir con el valor de retorno, porque el código de llamada administra la vida útil y la ubicación del parámetro y el valor de retorno.
Sin embargo, C++11 puede pasar de uno a otro. (En un ejemplo menos de juguete, se podría hacer algo al set
).
push_back
o insert
Finalmente, la elisión en contenedores no ocurre: pero C ++ 11 sobrecarga los operadores de inserción de movimiento de rvalue, lo que guarda copias.
struct whatever {
std::string data;
int count;
whatever( std::string d, int c ):data(d), count(c) {}
};
std::vector<whatever> v;
v.push_back( whatever("some long string goes here", 3) );
en C++03 un temporal whatever
se crea, luego se copia en el vector v
. 2 std::string
se asignan búferes, cada uno con datos idénticos, y uno se descarta.
En C++ 11 un temporal whatever
es creado. los whatever&&
push_back
sobrecarga entonces move
s que temporal en el vector v
. Una std::string
se asigna el búfer y se mueve al vector. Un vacío std::string
se descarta.
Asignación
Robado de la respuesta de @ Jarod42 a continuación.
La elisión no puede ocurrir con la asignación, pero sí puede ocurrir el traslado.
std::set<int> some_function();
std::set<int> some_value;
// code
some_value = some_function();
aquí some_function
devuelve un candidato para elidir, pero debido a que no se usa para construir un objeto directamente, no se puede elidir. En C++03, lo anterior da como resultado que el contenido del temporal se copie en some_value
. En C++ 11, se mueve a some_value
que básicamente es gratis.
Para obtener el efecto completo de lo anterior, necesita un compilador que sintetice los constructores de movimiento y la asignación por usted.
MSVC 2013 implementa constructores de movimiento en std
contenedores, pero no sintetiza constructores de movimiento en sus tipos.
Así que los tipos que contienen std::vector
s y similares no obtienen dichas mejoras en MSVC2013, pero comenzarán a obtenerlas en MSVC2015.
clang y gcc han implementado desde hace mucho tiempo constructores de movimientos implícitos. El compilador de Intel de 2013 admitirá la generación implícita de constructores de movimientos si aprueba -Qoption,cpp,--gen_move_operations
(no lo hacen de forma predeterminada en un esfuerzo por ser compatible con MSVC2013).
si tienes algo como:
std::vector<int> foo(); // function declaration.
std::vector<int> v;
// some code
v = foo();
Obtuviste una copia en C++03, mientras que obtuviste una asignación de movimiento en C++11. entonces tienes optimización gratuita en ese caso.
Recuerde que la elisión de copias y la optimización del valor de retorno se realizan cuando se construye un nuevo objeto usando un constructor de copias. Sin embargo, en un operador de asignación de copia, no hay elisión de copia (cómo puede ser, ya que el compilador no sabe qué hacer con un objeto ya construido que no es temporal). Por lo tanto, en ese caso, C ++ 11/14 gana a lo grande, al brindarle la posibilidad de usar un operador de asignación de movimiento. Sin embargo, sobre su pregunta, no creo que el código C ++ 98 deba ser más rápido si lo compila un compilador C ++ 11/14, tal vez sea más rápido porque el compilador es más nuevo.
– vsoftco
22 de diciembre de 2014 a las 1:13
Además, el código que usa la biblioteca estándar es potencialmente más rápido, incluso si lo hace totalmente compatible con C++ 98, porque en C++ 11/14, la biblioteca subyacente usa la semántica de movimiento interno cuando es posible. Por lo tanto, el código que parece idéntico en C++ 98 y C++ 11/14 será (posiblemente) más rápido en el último caso, cada vez que use los objetos de biblioteca estándar, como vectores, listas, etc., y mueva la semántica hace la diferencia.
– vsoftco
22 de diciembre de 2014 a las 1:22
@vsoftco, ese es el tipo de situación a la que me refería, pero no pude encontrar un ejemplo: por lo que recuerdo, si tengo que definir el constructor de copia, el constructor de movimiento no se generará automáticamente, lo que nos deja con clases muy simples donde RVO, creo, siempre funciona. Una excepción podría ser algo en conjunción con los contenedores STL, donde los constructores rvalue son generados por el implementador de la biblioteca (lo que significa que no tendría que cambiar nada en el código para que use movimientos).
– un gran
22 de diciembre de 2014 a las 1:26
las clases no necesitan ser simples para no tener un constructor de copia. C ++ prospera con la semántica de valores, y el constructor de copias, el operador de asignación, el destructor, etc. deberían ser la excepción.
– sp2danny
22 de diciembre de 2014 a las 1:50
@Eric Gracias por el enlace, fue interesante. Sin embargo, después de haberlo revisado rápidamente, las ventajas de velocidad parecen provenir principalmente de agregar
std::move
y mover constructores (lo que requeriría modificaciones al código existente). Lo único realmente relacionado con mi pregunta fue la oración “Obtiene ventajas de velocidad inmediatas simplemente recompilando”, que no está respaldada por ningún ejemplo (sí menciona STL en la misma diapositiva, como lo hice en mi pregunta, pero nada específico ). Estaba pidiendo algunos ejemplos. Si estoy leyendo mal las diapositivas, házmelo saber.– un gran
23 de diciembre de 2014 a las 11:25