Conocimiento profundo de RVO en C ++

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.

Enlace original

Este artículo es el contenido original de Alibaba Cloud y no se puede reproducir sin permiso.

Supongo que te gusta

Origin blog.csdn.net/weixin_43970890/article/details/112977983
Recomendado
Clasificación