programar - sintaxis de c++
¿Los compiladores de C++ 11 convierten las variables locales en valores cuando pueden durante la optimización del código? (4)
A veces es aconsejable dividir expresiones complicadas o largas en varios pasos, por ejemplo (la segunda versión no es más clara, pero es solo un ejemplo):
return object1(object2(object3(x)));
Se puede escribir como:
object3 a(x);
object2 b(a);
object1 c(b);
return c;
Suponiendo que las 3 clases implementan constructores que toman rvalue como parámetro, la primera versión puede ser más rápida, porque los objetos temporales se pasan y se pueden mover. Supongo que en la segunda versión, las variables locales se consideran lvalues. Pero si las variables no se usan más adelante, ¿los compiladores C ++ 11 optimizan el código para que las variables se consideren valores r y ambas versiones funcionan exactamente igual? Estoy muy interesado en el compilador de C ++ de Visual Studio 2013, pero también estoy feliz de saber cómo se comporta el compilador de GCC en este asunto.
Gracias, Michal
Pero si las variables no se usan más adelante, ¿los compiladores C ++ 11 optimizan el código para que las variables se consideren valores r y ambas versiones funcionan exactamente igual?
Es posible, pero depende mucho de tus tipos. Considere el siguiente ejemplo con un point
tipo POD:
#include <cstdio>
struct point {
int x;
int y;
};
static point translate(point p, int dx, int dy) {
return { p.x + dx, p.y + dy };
}
static point mirror(point p) {
return { -p.x, -p.y };
}
static point make_point(int x, int y) {
return { x, y };
}
int main() {
point a = make_point(1, 2);
point b = translate(a, 3, 3);
point c = mirror(b);
std::printf("(x,y) = (%d,%d)/n", c.x, c.y);
}
Miré el código de ensamblado, aquí es en lo que básicamente se compiló el programa (!) (Por lo que el siguiente código es una aproximación en C del código ensamblador generado):
int main() {
std::printf("(x,y) = (-4,-5)/n");
}
No solo eliminó todas las variables locales, sino que también hizo los cálculos en tiempo de compilación. He probado tanto gcc como clang pero no msvc.
OK, hagamos el programa un poco más complicado para que no pueda hacer los cálculos:
int main(int argc, char* argv[]) {
int x = *argv[1]-''0'';
int y = *argv[2]-''0'';
point a = make_point(x,y);
point b = translate(a, 3, 3);
point c = mirror(b);
std::printf("(x,y) = (%d,%d)/n", c.x, c.y);
}
Para ejecutar este código, debe llamarlo como ./a.out 1 2
.
Todo este programa se reduce a este (ensamblaje reescrito en C) después de la optimización:
int main(int argc, char* argv[]) {
int x = *argv[1]-''0'';
int y = *argv[2]-''0'';
std::printf("(x,y) = (%d,%d)/n", -(x+3), -(y+3));
}
Así que make_point()
a, b, c
y todas las funciones make_point()
, translate()
y mirror()
e hizo la mayor cantidad de cálculos posibles en el momento de la compilación.
Por las razones mencionadas en la respuesta de Matthieu M. , no espere que ocurran tan buenas optimizaciones con tipos más complicados (especialmente no POD).
En mi experiencia, inline es crucial. Trabaja duro para que tus funciones puedan incluirse fácilmente. Use optimizaciones de tiempo de enlace.
Como dijo juanchopanza, el compilador no puede (a nivel de C ++) violar la regla "como si"; eso es todas las transformaciones deberían producir un código semánticamente equivalente.
Sin embargo, más allá del nivel de C ++, cuando se optimiza el código, pueden surgir más oportunidades.
Como tal, realmente depende de los objetos en sí: si los constructores / destructores de movimiento tienen efectos secundarios, y la (des) asignación de memoria es un efecto secundario, entonces la optimización no puede ocurrir. Si solo usa PODs, con move-constructors / destructores por defecto, entonces probablemente se optimizará automáticamente.
El compilador no puede romper la regla "como si" en este caso. Pero puedes usar std::move
para lograr el efecto deseado:
object3 a(x);
object2 b(std::move(a));
object1 c(std::move(b));
return c;
Tenga en cuenta que además de la semántica de movimiento que puede acelerar su código, el compilador también está haciendo (N) RVO - (Nombrado) Optimización del valor de retorno, que en realidad puede darle aún más eficiencia a su código. He probado su ejemplo y en g ++ 4.8 parece que su segundo ejemplo podría ser más óptimo:
object3 a(x);
object2 b(a);
object1 c(b);
return c;
A partir de mis experimentos, parece que se llamaría constructor / destructor 8 veces (1 ctr + 2 copias ctrs + 1 movimiento ctr + 4 dtrs), en comparación con otro método que lo llama 10 veces (1 ctr + 4 movimientos ctors + 5 dtors) . Pero como el usuario2079303 ha comentado, los constructores de movimientos aún deben superar a los constructores de copias, también en este ejemplo todas las llamadas estarán en línea para que no se produzca una sobrecarga de llamadas de función.
Copiar / mover elisión es en realidad una excepción a la regla "como si", lo que significa que a veces puede sorprenderse de que su constructor / destructor incluso con efectos secundarios no sea llamado.
http://coliru.stacked-crooked.com/a/1ca7ebec0567e48f
(Puede desactivar (N) RVO con el parámetro -fno-elide-constructors)
#include <iostream>
#include <memory>
template<int S>
struct A {
A() { std::cout<<"A::A"<<std::endl; }
template<int S2>
A(const A<S2>&) { std::cout<<"A::A&"<<std::endl; }
template<int S2>
A(const A<S2>&&) { std::cout<<"A::A&&"<<std::endl; }
~A() { std::cout<<"~A::A"<<std::endl;}
};
A<0> foo () {
A<2> a; A<1> b(a); A<0> c(b); return c; // calls dtor/ctor 8 times
//return A<0>(A<1>(A<2>())); // calls dtor/ctor 10 times
}
int main()
{
A<0> a=foo();
return 0;
}