Hola a todos.
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:
//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;
}
Bien, tras pasar ambos mains por el mismo profiler, en la misma máquina (obvio) obtengo lo siguiente:
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.