Prefacio
Considere la existencia de una clase como HeavyObject cuya operación de asignación de copia requiere mucho tiempo. Por lo general, ¿a qué método estaría acostumbrado cuando usa una función para devolver un objeto de esta clase? ¿O elegirás un método determinado de acuerdo con el escenario específico?
// style 1
HeavyObject func(Args param);
// style 2
bool func(HeavyObject* ptr, Args param);
Los dos métodos anteriores pueden lograr el mismo propósito, pero la diferencia en la experiencia intuitiva también es muy obvia:
El estilo 1 solo requiere una línea de código, mientras que el estilo 2 requiere dos líneas de código
// style 1
HeavyObject obj = func(params);
// style 2
HeavyObject obj;
func(&obj, params);
Sin embargo, para lograr el mismo propósito, el costo puede no ser el mismo. Esto depende de muchos factores, como las características admitidas por el compilador, la especificación obligatoria del estándar del lenguaje C ++ y el desarrollo de varios equipos y entornos.
Parece que aunque el estilo 2 necesita escribir dos líneas de código al usarlo, el costo interno de la función está determinado y solo dependerá de su compilador actual. Incluso si usa un compilador diferente para llamar a la función externamente, no habrá ningún extra Problemas de estabilidad y costo de tiempo. Por ejemplo, func usa clang + libc ++ para compilar internamente, y el entorno de compilación para llamadas externas es gcc + gnustl o vc ++. Además de la sobrecarga de llamadas de función, no hay necesidad de preocuparse por otras sobrecargas de rendimiento y fallas debido a diferentes entornos de compilación.
Así que aquí analizo principalmente los puntos a los que los desarrolladores detrás del estilo 1 deben prestar atención.
RVO
RVO es la abreviatura de Return Value Optimization, es decir, optimización del valor de retorno. NRVO es la optimización del valor de retorno con nombre. Es una variante de RVO. Esta función es compatible con C ++ 11, lo que significa que tanto C ++ 98 como C ++ 03 Esta función de optimización no está escrita en el estándar, pero una pequeña cantidad de compiladores también admitirán la optimización RVO durante el proceso de desarrollo (¿como IBM Compiler?). Por ejemplo, Microsoft solo comenzó a admitirlo en Visual Studio 2010.
Aún tomando la clase HeavyObject anterior como ejemplo, para tener una comprensión más clara del comportamiento del compilador, la construcción / destrucción y la construcción de copias, las operaciones de asignación y los constructores rvalue se implementan aquí, de la siguiente manera
class HeavyObject
{
public:
HeavyObject() { cout << "Constructor\n"; }
~HeavyObject() { cout << "Destructor\n"; }
HeavyObject(HeavyObject const&) { cout << "Copy Constructor\n"; }
HeavyObject& operator=(HeavyObject const&) { cout << "Assignment Operator\n"; return *this; }
HeavyObject(HeavyObject&&) { cout << "Move Constructor\n"; }
private:
// many members omitted...
};
Entorno de compilación:
AppleClang 10.0.1.10010046
* La primera forma de uso
HeavyObject func()
{
return HeavyObject();
}
// call
HeavyObject o = func();
De acuerdo con la comprensión previa de C ++, el orden de construcción y destrucción de la clase HeavyObject debe ser
Constructor
Copiar Constructor
Destructor
Destructor
Pero la salida después de la operación real es
Constructor
Incinerador de basuras
En la operación real, el costo de construcción y destrucción de copias se pierde una vez, y el compilador nos ayuda a optimizarlo.
Así que lo desmonté:
0000000100000f60 <__Z4funcv>:
100000f60: 55 push %rbp
100000f61: 48 89 e5 mov %rsp,%rbp
100000f64: 48 83 ec 10 sub $0x10,%rsp
100000f68: 48 89 f8 mov %rdi,%rax
100000f6b: 48 89 45 f8 mov %rax,-0x8(%rbp)
100000f6f: e8 0c 00 00 00 callq 100000f80 <__ZN11HeavyObjectC1Ev>
100000f74: 48 8b 45 f8 mov -0x8(%rbp),%rax
100000f78: 48 83 c4 10 add $0x10,%rsp
100000f7c: 5d pop %rbp
100000f7d: c3 retq
100000f7e: 66 90 xchg %ax,%ax
El __Z4funcv en el código ensamblador anterior es la función func () y __ZN11HeavyObjectC1Ev es HeavyObject :: HeavyObject ().
Las reglas de decoración de C ++ de diferentes compiladores son ligeramente diferentes.
De hecho, aquí hay que crear primero el objeto externo y luego pasar la dirección del objeto externo como parámetro a la función func, similar al estilo 2.
* La segunda forma de usar
HeavyObject func()
{
HeavyObject o;
return o;
}
// call
HeavyObject o = func();
El resultado de ejecutar el código de llamada anterior es
Constructor
Incinerador de basuras
El resultado es el mismo que el del primer método de uso, aquí el compilador realmente hace NRVO, echemos un vistazo al desmontaje
0000000100000f40 <__Z4funcv>: // func()
100000f40: 55 push %rbp
100000f41: 48 89 e5 mov %rsp,%rbp
100000f44: 48 83 ec 20 sub $0x20,%rsp
100000f48: 48 89 f8 mov %rdi,%rax
100000f4b: c6 45 ff 00 movb $0x0,-0x1(%rbp)
100000f4f: 48 89 7d f0 mov %rdi,-0x10(%rbp)
100000f53: 48 89 45 e8 mov %rax,-0x18(%rbp)
100000f57: e8 24 00 00 00 callq 100000f80 <__ZN11HeavyObjectC1Ev> // HeavyObject::HeavyObject()
100000f5c: c6 45 ff 01 movb $0x1,-0x1(%rbp)
100000f60: f6 45 ff 01 testb $0x1,-0x1(%rbp)
100000f64: 0f 85 09 00 00 00 jne 100000f73 <__Z4funcv+0x33>
100000f6a: 48 8b 7d f0 mov -0x10(%rbp),%rdi
100000f6e: e8 2d 00 00 00 callq 100000fa0 <__ZN11HeavyObjectD1Ev> // HeavyObject::~HeavyObject()
100000f73: 48 8b 45 e8 mov -0x18(%rbp),%rax
100000f77: 48 83 c4 20 add $0x20,%rsp
100000f7b: 5d pop %rbp
100000f7c: c3 retq
100000f7d: 0f 1f 00 nopl (%rax)
En el código de ensamblaje anterior, puede ver que al devolver un objeto local con nombre, el compilador optimiza la operación para ejecutar el constructor directamente en el puntero del objeto externo como en la primera forma de uso, pero si la construcción falla, el destructor será llamado nuevamente. .
Las optimizaciones realizadas por los dos métodos de uso anteriores son muy similares. El punto común de los dos métodos es devolver un objeto local. Entonces, cuando hay varios objetos localmente y necesita elegir devolver un objeto según las condiciones, ¿cuál será el resultado? ?
* La tercera forma de usar
HeavyObject dummy(int index)
{
HeavyObject o[2];
return o[index];
}
// call
HeavyObject o = dummy(1);
El resultado después de correr es
Constructor
Constructor
Copiar Constructor
Destructor
Destructor
Destructor
A partir de los resultados de la operación, podemos ver que no se realiza ninguna optimización de RVO, y en este momento se llama al constructor de copia.
De las tres implementaciones anteriores, puede ver que si su función implementa una sola función, como operar solo en un objeto y regresar, el compilador optimizará RVO; si la implementación de la función es más compleja, puede involucrar múltiples operaciones. Cuando el objeto no está seguro de qué objeto devolver, el compilador no realizará la optimización RVO. En este momento, se llamará al constructor de copia de la clase cuando la función regrese.
Sin embargo, cuando solo hay un objeto local, ¿el compilador realizará la optimización RVO?
* La cuarta forma de uso
HeavyObject func()
{
return std::move(HeavyObject());
}
// call
HeavyObject o = func();
La salida real es
Constructor
Mover Constructor
Destructor
Destructor
La implementación de la función anterior devuelve directamente la referencia rvalue del objeto temporal. Desde el resultado de ejecución real, se llama al constructor Move, que obviamente es diferente del resultado del primer modo de uso. No es lo que esperaba llamar al constructor y al análisis solo una vez. Constructor, lo que significa que el compilador no hace RVO.
* La quinta forma de usar
HeavyObject func()
{
HeavyObject o;
return static_cast<HeavyObject&>(o);
}
// call
HeavyObject o = func();
La salida real es
Constructor
Copiar Constructor
Destructor
Destructor
La implementación de la función anterior devuelve directamente la referencia del objeto local. El resultado de la operación real aún llama al constructor de copia. No se espera que llame al constructor y al destructor solo una vez, lo que significa que el compilador no hace RVO.
Como se puede ver en los dos métodos de uso anteriores, cuando se devuelve un objeto y el tipo de objeto es inconsistente con el tipo de retorno, el compilador no ejecutará RVO. De hecho, el documento estándar de C ++ tiene la siguiente descripción:
en una declaración de retorno en una función con un tipo de retorno de clase, cuando la expresión es el nombre de un objeto automático no volátil (que no sea una función o un parámetro de cláusula de captura) con el mismo tipo cv-no calificado que el tipo de retorno de función, la operación de copiar / mover se puede omitir construyendo el objeto automático directamente en el valor de retorno de la función
para resumir
- El rendimiento de los dos códigos de estilo puede ser diferente. Cuando esté muy seguro sobre el entorno de desarrollo de su código y las funciones de soporte del compilador, como RVO, y el entorno de acceso del usuario, se recomienda utilizar el estilo 1; de lo contrario, se recomienda utilizar el estilo 2.
- Las funciones de optimización del compilador de RVO requieren restricciones relativamente estrictas. Cuando se usa el estilo 1, es posible que la implementación de funciones más complejas no se optimice con RVO como se espera.
Este artículo es el contenido original de Alibaba Cloud y no se puede reproducir sin permiso.