Para probar las gentilezas del estándar C++11, en concreto las referncias a rvalues y las semántica de movimientos he hecho un pequeño experimento.
Primero diré que las herramientas utilizadas son:
- Geany como IDE
- g++ versiones 4.6 y 4.7
- gprof
He hecho 2 programas, uno que es del clásico C++ (compilado con g++ v4.6) y otro que es C++11 con constructores de movimiento (compilado con g++ v4.7 -std=c++0x)
Tras esto he pasado por un profiler ambos programas y lo resultados me resultan tan extraños que no los puedo interpretar. Adjunto los códigos y las salidas del profiler:
Nota: el código es largo, pero es porque son 4 ficheros fuente y 2 cabceras completamente simples, no es nada duro de leer.
Código:
Bien, tras pasar ambos mains por el mismo profiler, en la misma máquina (obvio) obtengo lo siguiente://A.cpp: #include <cstring> #include "A.h" A::A(size_t sz=512):size(sz), buf(new char[size]){}; A::A(size_t sz,char *c):size(sz){ strcpy(this->buf,c); } A::~A(){ delete[] buf; }; A::A(const A &a){ this->size=a.size; this->buf=new char[size]; strcpy(this->buf,a.buf); } A& A::operator=(const A &a){ A *aux=new A(a.size,a.buf); return *aux; } //A.h #include <cstdlib> #ifndef A_H #define A_H 1 class A{ private: size_t size; char * buf; explicit A(size_t ,char *); public: explicit A(size_t ); ~A(); A(const A&); A & operator=(const A&); }; #endif //-------------------------------------------------------------- //B.cpp: #include <cstring> #include <cstddef> //<====== C++11 #include "B.h" B::B(size_t sz=512):size(sz), buf(new char[size]){}; B::B(size_t sz,char *c):size(sz){ strcpy(this->buf,c); } B::B(B &&other):size(0), buf(nullptr){ //Traemos a 'other' aquí this->size=other.size; this->buf=other.buf; //Reseteamos a 'other' para dejarlo en estado //consistente other.size=0; other.buf=nullptr; }; B::~B(){ delete[] buf; }; B::B(const B &a){ this->size=a.size; this->buf=new char[size]; strcpy(this->buf,a.buf); } B& B::operator=(const B &a){ B *aux=new B(a.size,a.buf); return *aux; } //B.h #include <cstdlib> #ifndef B_H #define B_H 1 class B{ private: size_t size; char * buf; explicit B(size_t ,char *); public: explicit B(size_t ); B(B &&); ~B(); B(const B&); B & operator=(const B&); }; #endif //-------------------------------------------------------------- //main1.cpp: #include <iostream> #include <vector> #include "A.h" using namespace std; int main(){ vector<A> vb; for(int j=0;j<200;j++){ for(int i=0;i<50000;i++){ vb.push_back(A(1024)); } for(int i=0;i<50000;i++){ vb.pop_back(); } } cout<<"Terminado"<<endl; return 0; } //main2.cpp #include <iostream> #include <vector> #include "B.h" using namespace std; int main(){ vector<B> vb; for(int j=0;j<200;j++){ for(int i=0;i<50000;i++){ vb.push_back(B(1024)); } for(int i=0;i<50000;i++){ vb.pop_back(); } } cout<<"Terminado main2"<<endl; return 0; }
Advierto que se ven muy mal las distintas columnas, adjunto un resumen al final con lo importante.
Resultado de main.cpp (sin move semantics, g++ v4.6):
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls us/call us/call name
33.33 0.01 0.01 1115535 0.01 0.01 A::A(A const&)
33.33 0.02 0.01 main
16.67 0.03 0.01 1050000 0.00 0.00 std::vector<A, std::allocator<A> >::pop_back()
16.67 0.03 0.01 1050000 0.00 0.01 std::vector<A, std::allocator<A> >::push_back(A const&)
0.00 0.03 0.00 2165535 0.00 0.00 A::~A()
0.00 0.03 0.00 1115535 0.00 0.00 operator new(unsigned int, void*)
0.00 0.03 0.00 1050000 0.00 0.00 A::A(unsigned int)
...
...
...
Resultado de main2.cpp (con move-semantics, g++ v4.7):
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls ms/call ms/call name
25.00 0.09 0.09 main
13.89 0.14 0.05 20065535 0.00 0.00 B::~B()
12.50 0.18 0.04 10000000 0.00 0.00 std::vector<B, std::allocator<B> >::pop_back()
11.11 0.23 0.04 10065535 0.00 0.00 B::B(B&&)
8.33 0.26 0.03 20065552 0.00 0.00 B&& std::forward<B>(std::remove_reference<B>::type&)
8.33 0.28 0.03 10000000 0.00 0.00 B::B(unsigned int)
5.56 0.30 0.02 10000000 0.00 0.00 void __gnu_cxx::new_allocator<B>::construct<B>(B*, B&&)
2.78 0.32 0.01 10000000 0.00 0.00 __gnu_cxx::new_allocator<B>::destroy(B*)
2.78 0.33 0.01 10000000 0.00 0.00 void std::vector<B, std::allocator<B> >::emplace_back<B>(B&&)
2.78 0.34 0.01 10000000 0.00 0.00 std::vector<B, std::allocator<B> >::push_back(B&&)
2.78 0.34 0.01 65569 0.00 0.00 bool std::operator!=<B*>(std::move_iterator<B*> const&, std::move_iterator<B*> const&)
...
...
...
Profiler del main.cpp (%time, cumulative_seconds, self_seconds, calls, self_us/call, total_us/call )
Constructor de copia:
33.33 | 0.01 | 0.01 | 1115535 | 0.01 | 0.01 A::A(A const&)
Destructor:
0.00 | 0.03 | 0.00 | 2165535 | 0.00 | 0.00
Profiler del main2.cpp (%time, cumulative_seconds, self_seconds, calls, self_us/call, total_us/call )
Constructor de movimiento:
11.11 | 0.23 | 0.04 10065535 | 0.00 | 0.00 B::B(B&&)
Destructor:
13.89 | 0.14 | 0.05 20065535 | 0.00 | 0.00
Bueno, os digo lo que no entiendo:
No sólo que el código con contructor de movimiento es mas lento, esque no entiendo por qué en el main.cpp hay 1kk y pico de llamadas al constructor y en el main2.cpp hay mas de 10kk.
Además en main.cpp hay 1050000 llamadas a push_back() y pop_back() mientras que en main2.cpp hay 10000000 llamadas a push_back() y pop_back(), y esto ya si que no lo entiendo, porque los bucles for son idénticos.
¿Alguien puede ayudarme?
Gracias de antemano.