Nas parte 1 e na parte 2, nós estudamos todos os métodos da Regra do 5 e sua importância para uma aplicação sem problemas de memória. Nesta terceira parte, vamos estudar otimização que o compilador faz chamada copy-elision. Vamos ver o seguinte exemplo compilado com a versão do C++14 com a flag no-elide-constructors.
Matrix matrixLValue(){
Matrix matrix {1, 1};
return matrix;
}
Matrix matrixRValue(){
return Matrix {1, 1};
}
int main() {
cout << "Exemplo 1" << endl;
Matrix matrix1 {matrixLValue()};
cout << "Exemplo 2" << endl;
Matrix matrix2 {matrixRValue()};
cout << "End program" << endl;
return 0;
}
----------Output-------------
// g++ -std=c++14 -fno-elide-constructors
Exemplo 1
Constructor Matrix 1x1 // matrix {1, 1} is constructed
Constructor Matrix 1x1 // constructor called by copy-constructor
Copy-Constructor Matrix 1x1 // temp Matrix from matrixLValue is copied to temp matrix from main
Destructor Matrix: Deallocate Matrix 1x1 // temp Matrix from matrixLValue is destroyed
Move-Constructor Matrix 1x1 // temp matrix from main is move-constructed to matrix1
Destructor Matrix: Deallocate Matrix 0x0 // temp matrix from main is destroyed
Exemplo 2
Constructor Matrix 1x1 // temp Matrix matrix {1, 1} constructed
Move-Constructor Matrix 1x1 // temp matrix from matrixRValue is moved to temp matrix from main
Destructor Matrix: Deallocate Matrix 0x0 // temp matrix from matrixRValue is destroyed
Move-Constructor Matrix 1x1 // temp matrix from main is move-constructed to matrix2
Destructor Matrix: Deallocate Matrix 0x0 // temp matrix from main is destroyed
End program
Destructor Matrix: Deallocate Matrix 1x1 // matrix1 destroyed
Destructor Matrix: Deallocate Matrix 1x1 // matrix2 destroyed
----------Output-------------
// g++ -std=c++14
Exemplo 1
Constructor Matrix 1x1 // NRVO. Matrix {1,1} is directed contructed in matrix1
Exemplo 2
Constructor Matrix 1x1 // NRO. Matrix {1,1} is directed contructed in matrix2
End program
Destructor Matrix: Deallocate Matrix 1x1 // matrix1 destroyed
Destructor Matrix: Deallocate Matrix 1x1 // matrix2 destroyed
É possível perceber que a primeira versão com a flag no-elide-constructors fez diversas operações de move-constructor e copy-constructor enquanto a segunda versão implementa uma otimização chamada copy-elision. Vamos analisar os 2 exemplos:
Matrix matrixLValue(){
Matrix matrix {1, 1};
return matrix;
}
O método matrixLValue retorna um LValue, pois matrix tem um endereço na memória. A otimização de evitar cópias e construir o objeto diretamente na variável matrix1 é chamada NRVO ou Named Return Value Optimisation. Observe que algumas condições precisam ser cumpridas para essa otimização ocorrer:
- O returno deve ser do tipo
return object;
. Caso o retorno seja uma expressão, não ocorrerá essa otimização. Por exemplo, se houver um ternário, o compilador irá recorrer ao copy-constructor tradicional:
Matrix matrixRValue(){
Matrix matrix {1, 1};
return true ? matrix : matrix;
}
int main() {
cout << "Exemplo 1" << endl;
Matrix matrix1 {matrixRValue()};
return 0;
}
----------Output-------------
Exemplo 1
Constructor Matrix 1x1
Constructor Matrix 1x1
Copy-Constructor Matrix 1x1
Destructor Matrix: Deallocate Matrix 1x1
Destructor Matrix: Deallocate Matrix 1x1
- O retorno deve ser uma variável local ou um parâmetro da função. Retornar uma propriedade de um objeto impede essa otimização.
- Mesmo se o copy-constructor ou move-constructor foram suprimidos pela otimização, é importante que eles existam para que essa otimização seja possível. O objeto deve copiável e movível.
- No C++17, não é obrigatório a ocorrência de NRVO.
Vamos agora analisar o segundo exemplo:
Matrix matrixRValue(){
return Matrix {1, 1};
}
Neste caso, o retorn é um RValue. A otimização para evitar o copy-constructor e move-constructor para esse caso é chamado RVO ou Retun Value Optimization. Algumas observações sobre o RVO são:
- O retorno deve ser do tipo return value; igual no NRVO. Não pode haver uma expressão
- No C++17, a otimização RVO é garantida.
Copy-elision consegue suprimir também o caso de múltiplas chamadas de move-constructor. Veja o seguinte exemplo:
int main() {
Matrix matrix {Matrix{Matrix{Matrix{1, 1}}}};
return 0;
}
----------Output-------------
// g++ -std=c++14 -fno-elide-constructors
Constructor Matrix 1x1
Move-Constructor Matrix 1x1
Move-Constructor Matrix 1x1
Move-Constructor Matrix 1x1
Destructor Matrix: Deallocate Matrix 0x0
Destructor Matrix: Deallocate Matrix 0x0
Destructor Matrix: Deallocate Matrix 0x0
Destructor Matrix: Deallocate Matrix 1x1
----------Output-------------
// g++ -std=c++14
Constructor Matrix 1x1
Destructor Matrix: Deallocate Matrix 1x1
Nessse caso, temos várias instâncias de Matrix criadas, que precisam ser move-constructed em sequência até a atribuição da variável matriz. O compilador consegue identificar que esses move-constructors não seriam necessários e os elimina, de forma que somente o construtor é chamado sem nenhum cópia ou movimentação. Obviamente, se o seu código dependesse desses move-constructors para funcionar, haveria um problema. Entretanto, esse deveria ser um cenário anormal, pois idealmente o seu código não deveria depender desse comportamento.
Conclusão
Copy-elision é uma otimização muito eficiente para eliminar cópias desnecessárias de objetos. Para os exemplos demonstrados, pode ser que a diferença de performance não seja relevante. Contudo, essa otimização pode ser muito impactante se o objeto for complexo para copiar como, por exemplo, na hora de redimensionar um vector.
Referências
- Gregoire, Marc. Professional C++. 5 Edição. Wrox
- Sehr, Viktor; Andrist Bjorn. C++ High Performance: Boost and optimize the performance of your C++17 code. Packt.
- https://ryonaldteofilo.medium.com/copy-move-elision-in-c-rvo-nrvo-f97b01d772ea