c++ - ¿Qué optimización proporciona la semántica de movimiento si ya tenemos RVO?
optimization c++11 (8)
Después de algunas excavaciones, encuentro este excelente ejemplo de optimización con referencias de valor en las Preguntas frecuentes de Stroustrup .
Sí, función de intercambio:
template<class T>
void swap(T& a, T& b) // "perfect swap" (almost)
{
T tmp = move(a); // could invalidate a
a = move(b); // could invalidate b
b = move(tmp); // could invalidate tmp
}
Esto generará código optimizado para cualquier tipo de tipos (asumiendo que tenga constructor de movimiento).
Edición: Además, RVO no puede optimizar algo como esto (al menos en mi compilador):
stuff func(const stuff& st)
{
if(st.x>0)
{
stuff ret(2*st.x);
return ret;
}
else
{
stuff ret2(-2*st.x);
return ret2;
}
}
Esta función siempre llama al constructor de copias (verificada con VC ++). Y si nuestra clase puede moverse más rápido, con el constructor de movimientos tendremos optimización.
Según tengo entendido, uno de los propósitos de agregar semántica de movimiento es optimizar el código llamando a un constructor especial para copiar objetos "temporales". Por ejemplo, en this respuesta vemos que se puede utilizar para optimizar dicha string a = x + y
cosas. Debido a que x + y es una expresión rvalue, en lugar de copiar en profundidad, solo podemos copiar el puntero a la cadena y al tamaño de la cadena. Pero como sabemos, los compiladores modernos admiten la optimización del valor de retorno , por lo que, sin utilizar la semántica de movimiento, nuestro código no llamará en absoluto al constructor de copia.
Para probarlo escribo este código:
#include <iostream>
struct stuff
{
int x;
stuff(int x_):x(x_){}
stuff(const stuff & g):x(g.x)
{
std::cout<<"copy"<<std::endl;
}
};
stuff operator+(const stuff& lhs,const stuff& rhs)
{
stuff g(lhs.x+rhs.x);
return g;
}
int main()
{
stuff a(5),b(7);
stuff c = a+b;
}
Y después de ejecutarlo en VC ++ 2010 y g ++ en modo optimizado, obtengo una salida vacía.
¿Qué tipo de optimización es, si sin ella mi código aún funciona más rápido? ¿Podría explicar lo que estoy entendiendo mal?
El ejemplo publicado solo toma referencias constantes de valores y, por lo tanto, no puede tener aplicada la semántica de movimientos de manera explícita, ya que no hay una sola referencia de valor de valores allí. ¿Cómo puede mover la semántica hacer que su código sea más rápido cuando implementó un tipo sin referencias de valor?
Además, su código ya está cubierto por RVO y NRVO. La semántica de movimiento se aplica a muchas más situaciones que las dos.
El hecho de que este caso en particular ya esté cubierto por una optimización existente no significa que no existan otros casos en los que las referencias de valor r sean útiles.
La construcción de movimientos permite la optimización incluso cuando el temporal se devuelve desde una función que no puede estar en línea (tal vez sea una llamada virtual, o mediante un puntero de función).
Esta línea llama al primer constructor.
stuff a(5),b(7);
El operador Plus se llama mediante referencias explícitas de valores comunes.
stuff c = a + b;
Dentro del método de sobrecarga del operador, no se ha llamado ningún constructor de copia. De nuevo, se llama solo al primer constructor.
stuff g(lhs.x+rhs.x);
La asignación se realiza con RVO, por lo que no se necesita copia. NINGUNA copia del objeto devuelto a ''c'' es necesaria.
stuff c = a+b;
Debido a que no hay una referencia std::cout
, el compilador cuida de su valor c
nunca se usa. Luego, todo el programa se elimina, resultando en un programa vacío.
Hay muchos lugares algunos de los cuales se mencionan en otras respuestas.
Uno importante es que al cambiar el tamaño de un std::vector
, moverá los objetos conscientes de movimientos de la ubicación de la memoria antigua a la nueva en lugar de copiar y destruir el original.
Además, las referencias de rvalue permiten el concepto de tipos móviles, esto es una diferencia semántica y no solo una optimización. unique_ptr
no fue posible en C ++ 03 por lo que tuvimos la abominación de auto_ptr
.
Imagina que tus cosas eran una clase con memoria asignada a un montón como una cadena, y que tenía la noción de capacidad. Dale un operador + = que aumentará la capacidad geométricamente. En C ++ 03 esto podría parecer:
#include <iostream>
#include <algorithm>
struct stuff
{
int size;
int cap;
stuff(int size_):size(size_)
{
cap = size;
if (cap > 0)
std::cout <<"allocating " << cap <<std::endl;
}
stuff(const stuff & g):size(g.size), cap(g.cap)
{
if (cap > 0)
std::cout <<"allocating " << cap <<std::endl;
}
~stuff()
{
if (cap > 0)
std::cout << "deallocating " << cap << ''/n'';
}
stuff& operator+=(const stuff& y)
{
if (cap < size+y.size)
{
if (cap > 0)
std::cout << "deallocating " << cap << ''/n'';
cap = std::max(2*cap, size+y.size);
std::cout <<"allocating " << cap <<std::endl;
}
size += y.size;
return *this;
}
};
stuff operator+(const stuff& lhs,const stuff& rhs)
{
stuff g(lhs.size + rhs.size);
return g;
}
También imagina que quieres agregar más de dos cosas a la vez:
int main()
{
stuff a(11),b(9),c(7),d(5);
std::cout << "start addition/n/n";
stuff e = a+b+c+d;
std::cout << "/nend addition/n";
}
Para mí esto se imprime:
allocating 11
allocating 9
allocating 7
allocating 5
start addition
allocating 20
allocating 27
allocating 32
deallocating 27
deallocating 20
end addition
deallocating 32
deallocating 5
deallocating 7
deallocating 9
deallocating 11
Cuento 3 asignaciones y 2 desasignaciones para calcular:
stuff e = a+b+c+d;
Ahora agregue movimiento semántica:
stuff(stuff&& g):size(g.size), cap(g.cap)
{
g.cap = 0;
g.size = 0;
}
...
stuff operator+(stuff&& lhs,const stuff& rhs)
{
return std::move(lhs += rhs);
}
Corriendo de nuevo me sale:
allocating 11
allocating 9
allocating 7
allocating 5
start addition
allocating 20
deallocating 20
allocating 40
end addition
deallocating 40
deallocating 5
deallocating 7
deallocating 9
deallocating 11
Ahora estoy a 2 asignaciones y 1 desasignaciones. Eso se traduce en un código más rápido.
La semántica de Move no debe considerarse como un dispositivo de optimización, incluso si pueden usarse como tales.
Si va a querer copias de objetos (ya sean parámetros de función o valores de retorno), RVO y copy elision harán el trabajo cuando puedan. Mover la semántica puede ayudar, pero son más poderosos que eso.
La semántica de movimiento es útil cuando se desea hacer algo diferente, ya sea que el objeto pasado sea temporal (luego se enlaza con una referencia de valor ) o un objeto "estándar" con un nombre (también llamado valor constante ). Si quieres, por ejemplo, robar los recursos de un objeto temporal, entonces quieres mover la semántica (ejemplo: puedes robar el contenido al que apunta std::unique_ptr
).
La semántica de Move le permite devolver objetos no copiables desde funciones, lo que no es posible con el estándar actual. Además, los objetos no copiables se pueden colocar dentro de otros objetos, y esos objetos se moverán automáticamente si los objetos contenidos son.
Los objetos no copiables son excelentes, ya que no te obligan a implementar un constructor de copias propenso a errores. La mayoría de las veces, la semántica de la copia realmente no tiene sentido, pero la semántica en movimiento sí (piénselo).
Esto también le permite usar clases movibles std::vector<T>
incluso si T
no es copiable. La plantilla de clase std::unique_ptr
también es una gran herramienta cuando se trata de objetos no copiables (por ejemplo, objetos polimórficos).
Otro buen ejemplo que se me ocurre. Imagina que estás implementando una biblioteca matricial y escribe un algoritmo que toma dos matrices y genera otra:
Matrix MyAlgorithm(Matrix U, Matrix V)
{
Transform(U); //doesn''t matter what this actually does, but it modifies U
Transform(V);
return U*V;
}
Tenga en cuenta que no puede pasar U y V por referencia constante, porque el algoritmo los ajusta. Teóricamente podría pasarlos por referencia, pero esto se vería grosero y dejaría U
y V
en algún estado intermedio (ya que se llama Transform(U)
), lo que puede no tener ningún sentido para la persona que llama, o simplemente no tiene ningún sentido matemático en Todo, ya que es solo una de las transformaciones del algoritmo interno. El código se ve mucho más limpio si los pasas por valor y usas la semántica de movimiento si no vas a usar U
y V
después de llamar a esta función:
Matrix u, v;
...
Matrix w = MyAlgorithm(u, v); //slow, but will preserve u and v
Matrix w = MyAlgorithm(move(u), move(v)); //fast, but will nullify u and v
Matrix w = MyAlgorithm(u, move(v)); //and you can even do this if you need one but not the other