c++ - dev - ¿Qué tan cierto es "Want Speed? Pase por valor "
c++17 (3)
Corrígeme si estoy equivocado. Digamos que tengo:
struct X
{
std::string mem_name;
X(std::string name)
: mem_name(std::move(name))
{}
...
};
struct Y
{
std::string mem_name;
Y(const std::string &name)
: mem_name(name)
{}
...
};
En el código de X
, el name
es obviamente una copia de cualquier argumento que pasó a X
, X
invoca el movimiento ctor de std::string
para inicializar mem_name
, ¿verdad?
Llamemos a eso una copia y luego muévase en X * ; dos operaciones: COPIA, MOVER .
En el código de Y
, el name
es una referencia constante, lo que significa que no hay una copia real del elemento porque estamos tratando directamente con el argumento pasado desde donde el objeto de Y
necesita ser creado. Pero luego mem_name
name
para inicializar mem_name
en Y
; una operación: COPIA . Sin duda, debería ser mucho más rápido (y preferible para mí)?
En la charla GN13 de Scott Meyer (alrededor del marco de tiempo 8:10 y 8:56), habla sobre "¿Quieres velocidad? Pasa por valor" y me preguntaba si hay alguna diferencia de rendimiento o pérdida en los argumentos de paso (o cadenas para ser preciso ) por referencia y pasando por valor "para ganar velocidad?"
Soy consciente del hecho de que pasar argumentos por valor puede ser costoso, especialmente cuando se trata de datos de gran tamaño.
Quizás (¿claro?) Hay algo que me falta en su charla?
La idea de "¿Quiere velocidad? Pase por valor" (1) es que a veces, la copia puede ser eliminada. Tomando sus clases X
e Y
, considere este uso:
// Simulating a complex operation returning a temporary:
std::string foo() { return "a" + std::string("b"); }
struct X
{
std::string mem_name;
X(std::string name): mem_name(std::move(name)) {}
};
struct Y
{
std::string mem_name;
Y(const std::string &name): mem_name(name) {}
};
int main()
{
X(foo());
Y(foo());
}
Ahora analicemos ambos casos de construcción.
X
primero. foo()
devuelve un temporal, que se usa para inicializar el name
objeto. Ese objeto luego se mueve a mem_name
. Tenga en cuenta que el compilador puede aplicar la Optimización del valor de retorno y construir el valor de retorno de foo()
(incluso el valor de retorno del operator+
) directamente en el espacio del name
. Entonces, en realidad, no hay copia, solo un movimiento.
Ahora analicemos Y
foo()
devuelve un temporal nuevamente, que está vinculado al name
referencia. Ahora no hay espacio "suministrado externamente" para el valor de retorno, por lo que debe construirse en su propio espacio y estar vinculado a la referencia. Luego se copia en mem_name
. Así que estamos haciendo una copia, no hay forma de evitarlo.
En resumen, el resultado es:
Si se transfiere un lvalue, tanto
X
comoY
realizarán una copia (X
al inicializar elname
,Y
al inicializarmem_name
). Además,X
realizará un movimiento (al inicializarmem_name
).Si se transfiere un valor r,
X
solo realizará un movimiento, mientras queY
debe realizar una copia.
En general, se espera que un movimiento sea una operación cuyos requisitos de tiempo sean comparables a los de pasar un puntero (que es lo que pasa por referencia). Entonces, en efecto, X
no es peor que Y
para lvalues, y mejor para rvalues.
Por supuesto, no es una regla absoluta, y debe tomarse con un grano de sal. En caso de duda, perfil.
(1) El enlace es propenso a estar temporalmente no disponible, y desde el 11-12-2014, parece estar roto (404). Una copia de los contenidos (aunque con un formato extraño) parece estar disponible en varios sitios de blogs:
Alternativamente, el contenido original podría ser accesible a través de la máquina de retorno .
También tenga en cuenta que el tema en general ha provocado una gran discusión. Buscar en el título en papel trae muchos seguimientos y contrapuntos. Para enumerar un ejemplo de uno de estos, hay "¿Quiere velocidad? No (siempre) pase por valor" por SO miembro juanchopanza
No hay reglas generales para la optmización. Pase-por-valor puede dar algunas grandes ganancias en C ++ 11 con semántica de movimiento, junto con copia elisión.
Si realmente quieres velocidad, Profile tu código.
Si realmente no te importa exponer referencias en tu API (que DEBE SER un signo de que internamente estarás copiando / asignando el objeto dado) entonces usar copias está bien.
La elisión de copia es más rápido que el movimiento, y si no puede eludirse (por varias razones, como una cadena de llamadas de función dependiente demasiado larga), C ++ garantiza la semántica de movimiento.